"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_assert_1 = require("node:assert");
const client_utils_1 = require("@fluid-internal/client-utils");
const container_definitions_1 = require("@fluidframework/container-definitions");
const internal_1 = require("@fluidframework/container-definitions/internal");
const internal_2 = require("@fluidframework/driver-definitions/internal");
const internal_3 = require("@fluidframework/runtime-definitions/internal");
const internal_4 = require("@fluidframework/runtime-utils/internal");
const internal_5 = require("@fluidframework/telemetry-utils/internal");
const internal_6 = require("@fluidframework/test-runtime-utils/internal");
const sinon_1 = __importDefault(require("sinon"));
const channelCollection_js_1 = require("../channelCollection.js");
const compressionDefinitions_js_1 = require("../compressionDefinitions.js");
const containerRuntime_js_1 = require("../containerRuntime.js");
const dataStoreRegistry_js_1 = require("../dataStoreRegistry.js");
const messageTypes_js_1 = require("../messageTypes.js");
const packageVersion_js_1 = require("../packageVersion.js");
const index_js_1 = require("../summary/index.js");
const testDataStoreMessage = {
    type: "op",
    content: { address: "test-address", contents: "test-contents" },
};
function genTestDataStoreMessage(contents) {
    return {
        type: "op",
        content: { address: "test-address", contents },
    };
}
function submitDataStoreOp(runtime, id, contents, localOpMetadata) {
    runtime.submitMessage({
        type: messageTypes_js_1.ContainerMessageType.FluidDataStoreOp,
        contents: {
            address: id,
            contents,
        },
    }, localOpMetadata);
}
const changeConnectionState = (runtime, connected, clientId) => {
    const audience = runtime.getAudience();
    audience.setCurrentClientId(clientId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- Modifying private property
    runtime._getClientId = () => clientId;
    runtime.setConnectionState(connected, clientId);
};
function isSignalEnvelope(obj) {
    return (typeof obj === "object" &&
        obj !== null &&
        "contents" in obj &&
        typeof obj.contents === "object" &&
        obj.contents !== null &&
        "content" in obj.contents &&
        "type" in obj.contents &&
        typeof obj.contents.type === "string" &&
        (!("address" in obj) || typeof obj.address === "string" || obj.address === undefined) &&
        (!("clientBroadcastSignalSequenceNumber" in obj) ||
            typeof obj.clientBroadcastSignalSequenceNumber === "number"));
}
let sandbox;
function stubChannelCollection(containerRuntime) {
    // Pass data store op right back to ContainerRuntime
    const reSubmitFake = sandbox
        .stub()
        .callsFake(containerRuntime.submitMessage.bind(containerRuntime));
    const stub = sinon_1.default.createStubInstance(
    // createSubInstance does not work with property methods (which are
    // used for stricter typing); so, override via a subclass here.
    class TestChannelCollection extends channelCollection_js_1.ChannelCollection {
        // @ts-expect-error -- redefine as instance method for stubbing
        reSubmitContainerMessage(..._args) { }
        // @ts-expect-error -- redefine as instance method for stubbing
        rollbackDataStoreOp(..._args) { }
    }, {
        setConnectionState: sandbox.stub(),
        reSubmitContainerMessage: reSubmitFake,
        rollbackDataStoreOp: sandbox.stub(),
        notifyStagingMode: sandbox.stub(),
        dispose: sandbox.stub(),
    });
    containerRuntime.channelCollection = stub;
    return stub;
}
function assertSignalContentIsAString(content) {
    (0, node_assert_1.strict)(typeof content === "string", "Signal content expected to be a string");
}
describe("Runtime", () => {
    const configProvider = (settings) => ({
        getRawConfig: (name) => settings[name],
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let submittedOps = [];
    let submittedSignals = [];
    let opFakeSequenceNumber = 1;
    let clock;
    before(() => {
        clock = sinon_1.default.useFakeTimers();
    });
    beforeEach(() => {
        submittedOps = [];
        opFakeSequenceNumber = 1;
        submittedSignals = [];
        sandbox = sinon_1.default.createSandbox();
    });
    afterEach(() => {
        clock.reset();
    });
    after(() => {
        clock.restore();
    });
    const mockClientId = "mockClientId";
    // Mock the storage layer so "submitSummary" works.
    const defaultMockStorage = {
        uploadSummaryWithContext: async (summary, context) => {
            return "fakeHandle";
        },
    };
    const getMockContext = (params = {}, clientId = mockClientId) => {
        const { settings = {}, logger = new internal_5.MockLogger(), mockStorage = defaultMockStorage, loadedFromVersion, baseSnapshot, connected = true, attachState = container_definitions_1.AttachState.Attached, } = params;
        const mockContext = {
            attachState,
            deltaManager: new internal_6.MockDeltaManager(),
            audience: new internal_6.MockAudience(),
            quorum: new internal_6.MockQuorumClients(),
            taggedLogger: (0, internal_5.mixinMonitoringContext)(logger, configProvider(settings)).logger,
            clientDetails: { capabilities: { interactive: true } },
            closeFn: (_error) => { },
            updateDirtyContainerState: (_dirty) => { },
            getLoadedFromVersion: () => loadedFromVersion,
            submitFn: (_type, contents, _batch, metadata) => {
                submittedOps.push({ ...contents, metadata }); // Note: this object shape is for testing only. Not representative of real ops.
                return opFakeSequenceNumber++;
            },
            submitSignalFn: (content, targetClientId) => {
                (0, node_assert_1.strict)(isSignalEnvelope(content), "Invalid signal envelope");
                submittedSignals.push({
                    envelope: content,
                    clientId,
                    targetClientId,
                }); // Note: this object shape is for testing only. Not representative of real signals.
            },
            clientId,
            connected,
            storage: mockStorage,
            baseSnapshot,
        };
        // Update the delta manager's last message which is used for validation during summarization.
        mockContext.deltaManager.lastMessage = {
            clientId: mockClientId,
            type: internal_2.MessageType.Operation,
            sequenceNumber: 0,
            timestamp: Date.now(),
            minimumSequenceNumber: 0,
            referenceSequenceNumber: 0,
            clientSequenceNumber: 0,
            contents: undefined,
        };
        return mockContext;
    };
    const mockProvideEntryPoint = async () => ({
        myProp: "myValue",
    });
    describe("Container Runtime", () => {
        describe("IdCompressor", () => {
            it("finalizes idRange on attach", async () => {
                const logger = new internal_5.MockLogger();
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                logger.clear();
                const compressor = containerRuntime.idCompressor;
                (0, node_assert_1.strict)(compressor !== undefined);
                compressor.generateCompressedId();
                containerRuntime.createSummary();
                const range = compressor.takeNextCreationRange();
                node_assert_1.strict.equal(range.ids, undefined, "All Ids should have been finalized after calling createSummary()");
            });
        });
        describe("Batching", () => {
            it("Default flush mode", async () => {
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.strictEqual(containerRuntime.flushMode, internal_3.FlushMode.TurnBased);
            });
            it("Override default flush mode using options", async () => {
                const runtimeOptions = {
                    flushMode: internal_3.FlushMode.Immediate,
                };
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.strictEqual(containerRuntime.flushMode, internal_3.FlushMode.Immediate);
            });
            it("Throws UsageError and calls closeFn for invalid flushMode", async () => {
                const containerErrors = [];
                const context = {
                    ...getMockContext(),
                    closeFn: (error) => {
                        if (error !== undefined) {
                            containerErrors.push(error);
                        }
                    },
                };
                // Cast through unknown to simulate a stale/invalid numeric flushMode value (e.g., the former FlushModeExperimental.Async = 2)
                const invalidFlushMode = 2;
                await node_assert_1.strict.rejects(containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: context,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: { flushMode: invalidFlushMode },
                    provideEntryPoint: mockProvideEntryPoint,
                }), (error) => {
                    node_assert_1.strict.ok(error instanceof internal_5.UsageError, "Should throw a UsageError for invalid flushMode");
                    return true;
                });
                node_assert_1.strict.strictEqual(containerErrors.length, 1, "closeFn should have been called with the error");
                node_assert_1.strict.ok(containerErrors[0] instanceof internal_5.UsageError, "closeFn should have been called with a UsageError");
            });
            it("Process empty batch", async () => {
                let batchBegin = 0;
                let batchEnd = 0;
                let callsToEnsure = 0;
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({
                        settings: {
                            "Fluid.ContainerRuntime.enableBatchIdTracking": true,
                        },
                    }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerRuntime.ensureNoDataModelChanges = (callback) => {
                    callback();
                    callsToEnsure++;
                };
                changeConnectionState(containerRuntime, false, mockClientId);
                // Not connected, so nothing is submitted on flush - just queued in PendingStateManager
                submitDataStoreOp(containerRuntime, "1", testDataStoreMessage, { emptyBatch: true });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                containerRuntime.flush();
                changeConnectionState(containerRuntime, true, mockClientId);
                containerRuntime.on("batchBegin", () => batchBegin++);
                containerRuntime.on("batchEnd", () => batchEnd++);
                containerRuntime.process({
                    clientId: mockClientId,
                    sequenceNumber: 10,
                    clientSequenceNumber: 1,
                    type: internal_2.MessageType.Operation,
                    contents: JSON.stringify({
                        type: "groupedBatch",
                        contents: [],
                    }),
                }, true);
                node_assert_1.strict.strictEqual(callsToEnsure, 1);
                node_assert_1.strict.strictEqual(batchBegin, 1);
                node_assert_1.strict.strictEqual(batchEnd, 1);
                node_assert_1.strict.strictEqual(containerRuntime.isDirty, false);
            });
            for (const enableBatchIdTracking of [true, undefined])
                it("Replaying ops should resend in correct order, with batch ID if applicable", async () => {
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext({
                            settings: {
                                "Fluid.ContainerRuntime.enableBatchIdTracking": enableBatchIdTracking, // batchId only stamped if true
                            },
                        }),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {},
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    const containerRuntime = runtime;
                    stubChannelCollection(containerRuntime);
                    changeConnectionState(containerRuntime, false, mockClientId);
                    // Not connected, so nothing is submitted on flush - just queued in PendingStateManager
                    submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                    containerRuntime.flush();
                    submitDataStoreOp(containerRuntime, "2", testDataStoreMessage);
                    changeConnectionState(containerRuntime, true, mockClientId);
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                    containerRuntime.flush();
                    node_assert_1.strict.strictEqual(submittedOps.length, 2);
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                    node_assert_1.strict.strictEqual(submittedOps[0].contents.address, "1");
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                    node_assert_1.strict.strictEqual(submittedOps[1].contents.address, "2");
                    function batchIdMatchesUnsentFormat(batchId) {
                        return (
                        // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- TODO: ADO#58523 Code owners should verify if this code change is safe and make it if so or update this comment otherwise
                        batchId !== undefined &&
                            batchId.length === "00000000-0000-0000-0000-000000000000_[-1]".length &&
                            batchId.endsWith("_[-1]"));
                    }
                    if (enableBatchIdTracking === true) {
                        (0, node_assert_1.strict)(batchIdMatchesUnsentFormat(
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        submittedOps[0].metadata?.batchId), "expected unsent batchId format (0)");
                        (0, node_assert_1.strict)(batchIdMatchesUnsentFormat(
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        submittedOps[1].metadata?.batchId), "expected unsent batchId format (0)");
                    }
                    else {
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        (0, node_assert_1.strict)(submittedOps[0].metadata?.batchId === undefined, "Expected no batchId (0)");
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        (0, node_assert_1.strict)(submittedOps[1].metadata?.batchId === undefined, "Expected no batchId (1)");
                    }
                });
            // NOTE: This test is examining a case that only occurs with an old Loader that doesn't tell ContainerRuntime when processing system ops.
            // In other words, when the MockDeltaManager bumps its lastSequenceNumber, ContainerRuntime.process would be called in the current code, but not with legacy loader.
            it("Inbound (non-runtime) op triggers flush due to refSeq changing", async () => {
                const submittedBatches = [];
                const mockContext = getMockContext();
                mockContext.submitBatchFn =
                    (messages, referenceSequenceNumber = -1) => {
                        submittedOps.push(...messages); // Reusing submittedOps since submitFn won't be invoked due to submitBatchFn's presence
                        submittedBatches.push({ messages, referenceSequenceNumber });
                        return 999; // CSN not used in test asserts below
                    };
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                // Submit the first message
                submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                node_assert_1.strict.strictEqual(submittedOps.length, 0, "No ops submitted yet");
                // Bump lastSequenceNumber and trigger the "op" event artificially to simulate processing a non-runtime op
                // This will trigger a flush, which allows us to safely submit more ops next
                const mockDeltaManager = mockContext.deltaManager;
                ++mockDeltaManager.lastSequenceNumber;
                mockDeltaManager.emit("op", {
                    clientId: mockClientId,
                    sequenceNumber: mockDeltaManager.lastSequenceNumber,
                    clientSequenceNumber: 1,
                    type: internal_2.MessageType.ClientJoin,
                    contents: "test content",
                });
                node_assert_1.strict.equal(submittedOps.length, 1, "Submitted op count wrong after first op");
                // Submit the second message
                submitDataStoreOp(containerRuntime, "2", {
                    type: "op",
                    content: { address: "test-address", contents: "test-contents2" },
                });
                node_assert_1.strict.equal(submittedOps.length, 1, "Second op not yet submitted (scheduled for next microtask)");
                // Wait for the next tick for the second message to be flushed
                await Promise.resolve();
                // Validate that the messages were submitted
                node_assert_1.strict.equal(submittedOps.length, 2, "Two messages should be submitted");
                node_assert_1.strict.deepEqual(submittedBatches, [
                    { messages: [submittedOps[0]], referenceSequenceNumber: 0 }, // The first op
                    { messages: [submittedOps[1]], referenceSequenceNumber: 1 }, // The second op
                ], "Two batches should be submitted with different refSeq");
            });
            it("IDs unfinalized due to disconnect are properly finalized after reconnect", async () => {
                // Start out disconnected since step 1 is to generate IDs before reconnect
                const connected = false;
                const mockContext = getMockContext({ connected });
                const mockDeltaManager = mockContext.deltaManager;
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: { enableRuntimeIdCompressor: "on" },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const compressor = containerRuntime.idCompressor;
                (0, node_assert_1.strict)(compressor !== undefined, "Expected idCompressor to be defined");
                // Generate an ID while disconnected, and take the creation range,
                // but do not submit the op to leave the range unfinalized (same as if the op were submitted but not sequenced).
                const id1 = compressor.generateCompressedId();
                compressor.takeNextCreationRange();
                // Re-connect – replayPendingStates releases unfinalized ranges
                // (no IdAllocation op is submitted at this point, but the next will contain the unfinalized range).
                changeConnectionState(containerRuntime, true, mockClientId);
                // Simulate a remote op arriving before we submit anything else.
                // Bump refSeq, emit the "op" event, and continue execution at the end of the microtask queue.
                // This is how Inbound Queue works, and it makes sure we get coverage of ref seq coherency in this test.
                ++mockDeltaManager.lastSequenceNumber;
                mockDeltaManager.emit("op", {});
                await Promise.resolve();
                // Generate another ID and submit a data store op referencing it.
                // At flush time, the JIT generateIdAllocationOp callback calls takeNextCreationRange.
                // Because the unfinalized range was released during replay, both IDs
                // are included in the resulting allocation range.
                const id2 = compressor.generateCompressedId();
                submitDataStoreOp(containerRuntime, "someDS", genTestDataStoreMessage({ id: id2 }));
                // Let the Outbox flush so we can check submittedOps length.
                // With JIT ID allocation, the ID alloc op is prepended to the main batch,
                // which gets grouped into a single grouped batch op.
                await Promise.resolve();
                node_assert_1.strict.strictEqual(submittedOps.length, 1, "Expected 1 grouped batch (containing ID Allocation + data op)");
                // The grouped batch contains the ID allocation as its first inner message.
                // Extract the ID allocation range from it to finalize.
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- test-only: extracting untyped grouped batch contents
                const groupedContents = submittedOps[0].contents;
                (0, node_assert_1.strict)(Array.isArray(groupedContents), "Expected grouped batch contents to be an array");
                node_assert_1.strict.strictEqual(groupedContents.length, 2, "Expected 2 inner messages in grouped batch");
                // Simulate processing the ID Allocation ack — finalize the range.
                // Without the call to resetUnfinalizedCreationRange in replayPendingStates,
                // this results in a "Ranges finalized out of order" error.
                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access -- test-only: extracting untyped grouped batch contents
                compressor.finalizeCreationRange(groupedContents[0].contents.contents);
                // Both IDs should now be finalized (positive final IDs in op space).
                // Without resetUnfinalizedCreationRange, id1 would remain a local-only
                // ID (negative) that could never be shared with other clients.
                const id1OpSpace = compressor.normalizeToOpSpace(id1);
                const id2OpSpace = compressor.normalizeToOpSpace(id2);
                (0, node_assert_1.strict)(id1OpSpace >= 0, "id1 should be finalized after allocation range is sequenced");
                (0, node_assert_1.strict)(id2OpSpace >= 0, "id2 should be finalized after allocation range is sequenced");
            });
        });
        const expectedOrderSequentiallyErrorMessage = "orderSequentially callback exception";
        describe("orderSequentially (rollback not enabled)", () => {
            for (const flushMode of [internal_3.FlushMode.TurnBased, internal_3.FlushMode.Immediate]) {
                const fakeClientId = "fakeClientId";
                describe(`orderSequentially with flush mode: ${internal_3.FlushMode[flushMode]}`, () => {
                    let containerRuntime;
                    let mockContext;
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    const submittedOpsMetadata = [];
                    const containerErrors = [];
                    const getMockContextForOrderSequentially = () => {
                        return {
                            attachState: container_definitions_1.AttachState.Attached,
                            deltaManager: new internal_6.MockDeltaManager(),
                            audience: new internal_6.MockAudience(),
                            quorum: new internal_6.MockQuorumClients(),
                            taggedLogger: new internal_5.MockLogger(),
                            supportedFeatures: new Map([["referenceSequenceNumbers", true]]),
                            clientDetails: { capabilities: { interactive: true } },
                            closeFn: (error) => {
                                if (error !== undefined) {
                                    containerErrors.push(error);
                                }
                            },
                            updateDirtyContainerState: (_dirty) => { },
                            submitFn: (_type, 
                            // eslint-disable-next-line @typescript-eslint/no-explicit-any
                            contents, _batch, appData) => {
                                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                                if (contents.type === "groupedBatch") {
                                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                                    for (const subMessage of contents.contents) {
                                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                                        submittedOpsMetadata.push(subMessage.metadata);
                                    }
                                }
                                else {
                                    submittedOpsMetadata.push(appData);
                                }
                                return opFakeSequenceNumber++;
                            },
                            connected: true,
                            clientId: fakeClientId,
                            getLoadedFromVersion: () => undefined,
                        };
                    };
                    const getFirstContainerError = () => {
                        node_assert_1.strict.ok(containerErrors.length > 0, "Container should have errors");
                        return containerErrors[0];
                    };
                    beforeEach(async () => {
                        mockContext = getMockContextForOrderSequentially();
                        const runtimeOptions = {
                            summaryOptions: {
                                summaryConfigOverrides: {
                                    state: "disabled",
                                },
                            },
                            flushMode,
                        };
                        const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                            context: mockContext,
                            registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                            existing: false,
                            runtimeOptions,
                            provideEntryPoint: mockProvideEntryPoint,
                        });
                        containerRuntime = runtime;
                        containerErrors.length = 0;
                        submittedOpsMetadata.length = 0;
                    });
                    it("Can't call flush() inside orderSequentially's callback", () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => {
                            // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                            containerRuntime.flush();
                        }));
                        const error = getFirstContainerError();
                        (0, node_assert_1.strict)((0, internal_5.isFluidError)(error));
                        node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.genericError);
                        node_assert_1.strict.strictEqual(error.message, "0x24c");
                        node_assert_1.strict.strictEqual(error.getTelemetryProperties().orderSequentiallyCalls, 1);
                    });
                    it("Can't call flush() inside orderSequentially's callback when nested", () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => containerRuntime.orderSequentially(() => {
                            // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                            containerRuntime.flush();
                        })));
                        const error = getFirstContainerError();
                        (0, node_assert_1.strict)((0, internal_5.isFluidError)(error));
                        node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.genericError);
                        node_assert_1.strict.strictEqual(error.message, "0x24c");
                        node_assert_1.strict.strictEqual(error.getTelemetryProperties().orderSequentiallyCalls, 2);
                    });
                    it("Can't call flush() inside orderSequentially's callback when nested ignoring exceptions", () => {
                        containerRuntime.orderSequentially(() => {
                            try {
                                containerRuntime.orderSequentially(() => {
                                    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                                    containerRuntime.flush();
                                });
                            }
                            catch {
                                // ignore
                            }
                        });
                        const error = getFirstContainerError();
                        (0, node_assert_1.strict)((0, internal_5.isFluidError)(error));
                        node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.genericError);
                        node_assert_1.strict.strictEqual(error.message, "0x24c");
                        node_assert_1.strict.strictEqual(error.getTelemetryProperties().orderSequentiallyCalls, 2);
                    });
                    it("Errors propagate to the container", () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => {
                            throw new Error("Any");
                        }));
                        const error = getFirstContainerError();
                        (0, node_assert_1.strict)((0, internal_5.isFluidError)(error));
                        node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.genericError);
                        node_assert_1.strict.strictEqual(error.message, `${expectedOrderSequentiallyErrorMessage}: Any`);
                        node_assert_1.strict.strictEqual(error.getTelemetryProperties().orderSequentiallyCalls, 1);
                    });
                    it("Errors propagate to the container when nested", () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => containerRuntime.orderSequentially(() => {
                            throw new Error("Any");
                        })));
                        const error = getFirstContainerError();
                        (0, node_assert_1.strict)((0, internal_5.isFluidError)(error));
                        node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.genericError);
                        node_assert_1.strict.strictEqual(error.message, `${expectedOrderSequentiallyErrorMessage}: Any`);
                        node_assert_1.strict.strictEqual(error.getTelemetryProperties().orderSequentiallyCalls, 2);
                    });
                    it("Batching property set properly", () => {
                        containerRuntime.orderSequentially(() => {
                            submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "2", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "3", testDataStoreMessage);
                        });
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                        containerRuntime.flush();
                        node_assert_1.strict.strictEqual(submittedOpsMetadata.length, 3, "3 messages should be sent");
                        node_assert_1.strict.strictEqual(
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        submittedOpsMetadata[0].batch, true, "first message should be the batch start");
                        node_assert_1.strict.strictEqual(submittedOpsMetadata[1], undefined, "second message should not hold batch info");
                        node_assert_1.strict.strictEqual(
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        submittedOpsMetadata[2].batch, false, "third message should be the batch end");
                    });
                    it("Resubmitting batch preserves original batches", async () => {
                        stubChannelCollection(containerRuntime);
                        changeConnectionState(containerRuntime, false, fakeClientId);
                        containerRuntime.orderSequentially(() => {
                            submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "2", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "3", testDataStoreMessage);
                        });
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                        containerRuntime.flush();
                        containerRuntime.orderSequentially(() => {
                            submitDataStoreOp(containerRuntime, "4", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "5", testDataStoreMessage);
                            submitDataStoreOp(containerRuntime, "6", testDataStoreMessage);
                        });
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                        containerRuntime.flush();
                        node_assert_1.strict.strictEqual(submittedOpsMetadata.length, 0, "no messages should be sent");
                        changeConnectionState(containerRuntime, true, fakeClientId);
                        node_assert_1.strict.strictEqual(submittedOpsMetadata.length, 6, "6 messages should be sent");
                        const expectedBatchMetadata = [
                            { batch: true },
                            undefined,
                            { batch: false },
                            { batch: true },
                            undefined,
                            { batch: false },
                        ];
                        node_assert_1.strict.deepStrictEqual(submittedOpsMetadata, expectedBatchMetadata, "batch metadata does not match");
                    });
                });
            }
        });
        describe("orderSequentially with rollback", () => {
            for (const flushMode of [internal_3.FlushMode.TurnBased, internal_3.FlushMode.Immediate]) {
                describe(`orderSequentially with flush mode: ${internal_3.FlushMode[flushMode]}`, () => {
                    let containerRuntime;
                    const containerErrors = [];
                    let submittedOpsCount = 0;
                    const getMockContextForOrderSequentially = () => ({
                        attachState: container_definitions_1.AttachState.Attached,
                        connected: true,
                        clientId: "client-id",
                        supportedFeatures: new Map([["referenceSequenceNumbers", true]]),
                        deltaManager: new internal_6.MockDeltaManager(),
                        audience: new internal_6.MockAudience(),
                        quorum: new internal_6.MockQuorumClients(),
                        taggedLogger: (0, internal_5.mixinMonitoringContext)(new internal_5.MockLogger(), configProvider({
                            "Fluid.ContainerRuntime.EnableRollback": true,
                        })),
                        clientDetails: { capabilities: { interactive: true } },
                        closeFn: (error) => {
                            if (error !== undefined) {
                                containerErrors.push(error);
                            }
                        },
                        submitFn: (...args) => {
                            return ++submittedOpsCount; // clientSequenceNumber
                        },
                        updateDirtyContainerState: (dirty) => { },
                        getLoadedFromVersion: () => undefined,
                    });
                    beforeEach(async () => {
                        const runtimeOptions = {
                            summaryOptions: {
                                summaryConfigOverrides: { state: "disabled" },
                            },
                            flushMode,
                        };
                        const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                            context: getMockContextForOrderSequentially(),
                            registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                            existing: false,
                            runtimeOptions,
                            provideEntryPoint: mockProvideEntryPoint,
                        });
                        containerRuntime = runtime;
                        containerErrors.length = 0;
                        submittedOpsCount = 0;
                    });
                    it("No errors propagate to the container on rollback", () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => {
                            throw new Error("Any");
                        }));
                        node_assert_1.strict.equal(containerRuntime.inStagingMode, false, "Still in Staging Mode");
                        node_assert_1.strict.equal(containerRuntime.isDirty, false, "Dirty after rollback");
                        node_assert_1.strict.strictEqual(containerErrors.length, 0);
                    });
                    it("No errors on successful callback with rollback set", () => {
                        containerRuntime.orderSequentially(() => { });
                        node_assert_1.strict.equal(containerRuntime.inStagingMode, false, "Still in Staging Mode");
                        node_assert_1.strict.strictEqual(containerErrors.length, 0);
                    });
                    it("orderSequentially while in StagingMode works", async () => {
                        stubChannelCollection(containerRuntime);
                        const stageControls = containerRuntime.enterStagingMode();
                        containerRuntime.orderSequentially(() => {
                            submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                        });
                        node_assert_1.strict.strictEqual(submittedOpsCount, 0, "No ops should be submitted in Staging Mode");
                        stageControls.commitChanges();
                        node_assert_1.strict.strictEqual(submittedOpsCount, 1, "One (Grouped) op should have been submitted");
                    });
                    // The purpose of these tests is to illustrate the current behavior of this unspecified scenario
                    describe("Exiting staging mode under orderSequentially - expected behavior has not been specified", () => {
                        it("commitChanges under orderSequentially (happens to fail)", async () => {
                            stubChannelCollection(containerRuntime);
                            const stageControls = containerRuntime.enterStagingMode();
                            node_assert_1.strict.throws(() => {
                                containerRuntime.orderSequentially(() => {
                                    submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                                    stageControls.commitChanges();
                                });
                            });
                        });
                        it("discardChanges under orderSequentially (does not happen to fail)", async () => {
                            stubChannelCollection(containerRuntime);
                            const stageControls = containerRuntime.enterStagingMode();
                            node_assert_1.strict.doesNotThrow(() => {
                                containerRuntime.orderSequentially(() => {
                                    submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                                    stageControls.discardChanges();
                                });
                            });
                        });
                    });
                    it("Entering Staging Mode under orderSequentially not supported", async () => {
                        node_assert_1.strict.throws(() => containerRuntime.orderSequentially(() => {
                            containerRuntime.enterStagingMode();
                        }), (e) => e.errorType === internal_1.ContainerErrorTypes.usageError &&
                            e.message === "Already in staging mode", "Entering Staging Mode inside orderSequentially should throw");
                        node_assert_1.strict.equal(containerRuntime.inStagingMode, false, "Still in Staging Mode");
                        node_assert_1.strict.equal(containerRuntime.isDirty, false, "Dirty after rollback");
                        node_assert_1.strict.strictEqual(containerErrors.length, 0);
                    });
                });
            }
        });
        describe("Dirty flag", () => {
            const createMockContext = (attachState, addPendingMsg) => {
                const pendingState = {
                    pending: {
                        pendingStates: [
                            {
                                type: "message",
                                content: `{"type": "${messageTypes_js_1.ContainerMessageType.BlobAttach}", "contents": {}}`,
                            },
                        ],
                    },
                    savedOps: [],
                };
                return {
                    deltaManager: new internal_6.MockDeltaManager(),
                    audience: new internal_6.MockAudience(),
                    quorum: new internal_6.MockQuorumClients(),
                    taggedLogger: new internal_5.MockLogger(),
                    clientDetails: { capabilities: { interactive: true } },
                    updateDirtyContainerState: (_dirty) => { },
                    attachState,
                    pendingLocalState: addPendingMsg ? pendingState : undefined,
                    getLoadedFromVersion: () => undefined,
                };
            };
            it("should NOT be set to dirty if context is attached with no pending ops", async () => {
                const mockContext = createMockContext(container_definitions_1.AttachState.Attached, false);
                const updateDirtyStateStub = sandbox.stub(mockContext, "updateDirtyContainerState");
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: undefined,
                    containerScope: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.calledOnce, true);
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.args, [[false]]);
            });
            it("should be set to dirty if context is attached with pending ops", async () => {
                const mockContext = createMockContext(container_definitions_1.AttachState.Attached, true);
                const updateDirtyStateStub = sandbox.stub(mockContext, "updateDirtyContainerState");
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.calledOnce, true);
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.args, [[true]]);
            });
            it("should be set to dirty if context is attaching", async () => {
                const mockContext = createMockContext(container_definitions_1.AttachState.Attaching, false);
                const updateDirtyStateStub = sandbox.stub(mockContext, "updateDirtyContainerState");
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.calledOnce, true);
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.args, [[true]]);
            });
            it("should be set to dirty if context is detached", async () => {
                const mockContext = createMockContext(container_definitions_1.AttachState.Detached, false);
                const updateDirtyStateStub = sandbox.stub(mockContext, "updateDirtyContainerState");
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.calledOnce, true);
                node_assert_1.strict.deepStrictEqual(updateDirtyStateStub.args, [[true]]);
            });
        });
        describe("Pending state progress tracking", () => {
            const maxReconnects = 7; // 7 is the default used by ContainerRuntime for the max reconnection attempts
            let containerRuntime;
            const mockLogger = new internal_5.MockLogger();
            const containerErrors = [];
            const fakeClientId = "fakeClientId";
            const getMockContextForPendingStateProgressTracking = () => {
                return {
                    connected: false,
                    attachState: container_definitions_1.AttachState.Attached,
                    deltaManager: new internal_6.MockDeltaManager(),
                    audience: new internal_6.MockAudience(),
                    quorum: new internal_6.MockQuorumClients(),
                    taggedLogger: mockLogger,
                    clientDetails: { capabilities: { interactive: true } },
                    closeFn: (error) => {
                        if (error !== undefined) {
                            containerErrors.push(error);
                        }
                    },
                    updateDirtyContainerState: (_dirty) => { },
                    getLoadedFromVersion: () => undefined,
                };
            };
            const getMockPendingStateManager = () => {
                let pendingMessages = 0;
                return {
                    replayPendingStates: () => [],
                    hasPendingMessages: () => pendingMessages > 0,
                    hasPendingUserChanges: () => pendingMessages > 0,
                    processInboundMessages: (inbound, _local) => {
                        const messages = inbound.type === "fullBatch" ? inbound.messages : [inbound.nextMessage];
                        return messages.map((message) => ({
                            message,
                            localOpMetadata: undefined,
                        }));
                    },
                    get pendingMessagesCount() {
                        return pendingMessages;
                    },
                    onFlushBatch: (batch, _csn) => (pendingMessages += batch.length),
                };
            };
            const getMockChannelCollection = () => {
                // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                return {
                    processMessages: (..._args) => { },
                    setConnectionState: (..._args) => { },
                };
            };
            const getFirstContainerError = () => {
                node_assert_1.strict.ok(containerErrors.length > 0, "Container should have errors");
                return containerErrors[0];
            };
            beforeEach(async () => {
                containerErrors.length = 0;
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContextForPendingStateProgressTracking(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions: {
                        summaryOptions: {
                            summaryConfigOverrides: {
                                state: "disabled",
                            },
                        },
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
            });
            function patchRuntime(pendingStateManager, _maxReconnects = undefined) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Modifying private properties
                const runtime = containerRuntime;
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                runtime.pendingStateManager = pendingStateManager;
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                runtime.channelCollection = getMockChannelCollection();
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
                runtime.maxConsecutiveReconnects = _maxReconnects ?? runtime.maxConsecutiveReconnects;
            }
            /**
             * Connects with a new clientId and then immediately disconnects, returning that brief connection's clientId
             */
            const toggleConnection = (runtime, salt) => {
                const clientId = salt === undefined ? fakeClientId : `${fakeClientId}-${salt}`;
                changeConnectionState(runtime, true, clientId);
                changeConnectionState(runtime, false, clientId);
                return clientId;
            };
            const addPendingMessage = (pendingStateManager) => pendingStateManager.onFlushBatch([
                {
                    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                    runtimeOp: {
                        type: messageTypes_js_1.ContainerMessageType.FluidDataStoreOp,
                        contents: "",
                    },
                    referenceSequenceNumber: 0,
                },
            ], 1, false /* staged */);
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} connection state changes, with pending state, should ` +
                "generate telemetry event and throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager);
                for (let i = 0; i < maxReconnects; i++) {
                    addPendingMessage(pendingStateManager);
                    toggleConnection(containerRuntime, i);
                }
                // NOTE: any errors returned by getFirstContainerError() are from a variable set in a mock closeFn function passed
                // around during test setup, which executes when the container runtime causes the context (container) to close.
                const error = getFirstContainerError();
                node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.dataProcessingError);
                node_assert_1.strict.strictEqual(error.message, "Runtime detected too many reconnects with no progress syncing local ops.");
                (0, node_assert_1.strict)((0, internal_5.isILoggingError)(error));
                node_assert_1.strict.strictEqual(error.getTelemetryProperties().attempts, maxReconnects);
                node_assert_1.strict.strictEqual(error.getTelemetryProperties().pendingMessages, maxReconnects);
                mockLogger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                        attempts: 3,
                        pendingMessages: 3,
                    },
                ]);
            });
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} / 2 connection state changes, with pending state, should ` +
                "generate telemetry event but not throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager);
                addPendingMessage(pendingStateManager);
                for (let i = 0; i < maxReconnects / 2 + 1; i++) {
                    toggleConnection(containerRuntime, i);
                }
                // The particulars of the setup for this test mean that no errors here indicate the container did not close.
                node_assert_1.strict.equal(containerErrors.length, 0);
                mockLogger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                        attempts: 3,
                        pendingMessages: 1,
                    },
                ]);
            });
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} connection state changes, with pending state, with ` +
                "feature disabled, should not generate telemetry event nor throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager, -1 /* maxConsecutiveReconnects */);
                for (let i = 0; i < maxReconnects; i++) {
                    addPendingMessage(pendingStateManager);
                    toggleConnection(containerRuntime, i);
                }
                // The particulars of the setup for this test mean that no errors here indicate the container did not close.
                node_assert_1.strict.equal(containerErrors.length, 0);
                mockLogger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                    },
                ]);
            });
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} connection state changes, with no pending state, should ` +
                "not generate telemetry event nor throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager);
                for (let i = 0; i < maxReconnects; i++) {
                    toggleConnection(containerRuntime, i);
                }
                // The particulars of the setup for this test mean that no errors here indicate the container did not close.
                node_assert_1.strict.equal(containerErrors.length, 0);
                mockLogger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                    },
                ]);
            });
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} connection state changes, with pending state, successfully ` +
                "processing local op, should not generate telemetry event nor throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager);
                addPendingMessage(pendingStateManager);
                for (let i = 0; i < maxReconnects; i++) {
                    const clientId = toggleConnection(containerRuntime, i);
                    containerRuntime.process({
                        type: "op",
                        clientId,
                        sequenceNumber: i,
                        contents: {
                            address: "address",
                        },
                        clientSequenceNumber: 0,
                        minimumSequenceNumber: 0,
                    }, true /* local */);
                }
                // The particulars of the setup for this test mean that no errors here indicate the container did not close.
                node_assert_1.strict.equal(containerErrors.length, 0);
                mockLogger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                    },
                ]);
            });
            // biome-ignore format: https://github.com/biomejs/biome/issues/4202
            it(`No progress for ${maxReconnects} connection state changes, with pending state, successfully ` +
                "processing remote op and local chunked op, should generate telemetry event and throw an error that closes the container", async () => {
                const pendingStateManager = getMockPendingStateManager();
                patchRuntime(pendingStateManager);
                let seqNum = 1;
                for (let i = 0; i < maxReconnects; i++) {
                    addPendingMessage(pendingStateManager);
                    toggleConnection(containerRuntime, i);
                    containerRuntime.process({
                        type: "op",
                        clientId: `a unique, remote clientId - ${i}`,
                        sequenceNumber: seqNum++,
                        clientSequenceNumber: 1,
                        contents: {
                            address: "address",
                        },
                        minimumSequenceNumber: 0,
                    }, false /* local */);
                    containerRuntime.process({
                        type: "op",
                        clientId: "clientId",
                        sequenceNumber: seqNum++,
                        contents: {
                            address: "address",
                            contents: {
                                chunkId: i + 1,
                                totalChunks: maxReconnects + 1,
                            },
                            type: "chunkedOp",
                        },
                        minimumSequenceNumber: 0,
                    }, true /* local */);
                }
                // NOTE: any errors returned by getFirstContainerError() are from a variable set in a mock closeFn function passed
                // around during test setup, which executes when the container runtime causes the context (container) to close.
                const error = getFirstContainerError();
                node_assert_1.strict.strictEqual(error.errorType, internal_1.ContainerErrorTypes.dataProcessingError);
                node_assert_1.strict.strictEqual(error.message, "Runtime detected too many reconnects with no progress syncing local ops.");
                (0, node_assert_1.strict)((0, internal_5.isILoggingError)(error));
                node_assert_1.strict.strictEqual(error.getTelemetryProperties().attempts, maxReconnects);
                node_assert_1.strict.strictEqual(error.getTelemetryProperties().pendingMessages, maxReconnects);
                mockLogger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ReconnectsWithNoProgress",
                        attempts: 3,
                        pendingMessages: 3,
                    },
                ]);
            });
        });
        describe("Unrecognized types not supported", () => {
            let containerRuntime;
            beforeEach(async () => {
                const runtimeOptions = {
                    enableGroupedBatching: false,
                };
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions,
                    provideEntryPoint: mockProvideEntryPoint,
                }));
            });
            /**
             * Overwrites channelCollection property and exposes private submit function with modified typing
             */
            function patchContainerRuntime() {
                const patched = containerRuntime;
                patched.channelCollection = {
                    setConnectionState: (_connected, _clientId) => { },
                    // Pass data store op right back to ContainerRuntime
                    reSubmitContainerMessage: containerRuntime.submitMessage.bind(containerRuntime),
                };
                return patched;
            }
            it("Op with unrecognized type is ignored by resubmit", async () => {
                const patchedContainerRuntime = patchContainerRuntime();
                changeConnectionState(patchedContainerRuntime, false, mockClientId);
                submitDataStoreOp(patchedContainerRuntime, "1", testDataStoreMessage);
                submitDataStoreOp(patchedContainerRuntime, "2", testDataStoreMessage);
                patchedContainerRuntime.submit({
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
                    type: "FUTURE_TYPE",
                    contents: "3",
                });
                submitDataStoreOp(patchedContainerRuntime, "4", testDataStoreMessage);
                node_assert_1.strict.strictEqual(submittedOps.length, 0, "no messages should be sent while disconnected");
                // Connect, which will trigger resubmit
                node_assert_1.strict.throws(() => changeConnectionState(patchedContainerRuntime, true, mockClientId), (error) => error.errorType === internal_1.ContainerErrorTypes.dataProcessingError, "Ops with unrecognized type and 'Ignore' compat behavior should fail to resubmit");
            });
            it("process remote op with unrecognized type", async () => {
                const futureRuntimeMessage = {
                    type: "FROM_THE_FUTURE",
                    contents: "Hello",
                };
                const packedOp = {
                    contents: JSON.stringify(futureRuntimeMessage),
                    type: internal_2.MessageType.Operation,
                    sequenceNumber: 123,
                    clientId: "someClientId",
                    minimumSequenceNumber: 0,
                };
                node_assert_1.strict.throws(() => containerRuntime.process(packedOp, false /* local */), (error) => error.errorType === internal_1.ContainerErrorTypes.dataProcessingError, "Ops with unrecognized type should fail to process");
            });
        });
        describe("Supports mixin classes", () => {
            it("new loadRuntime2 method works", async () => {
                const makeMixin = (Base, methodName, methodReturn) => class MixinContainerRuntime extends Base {
                    static async loadRuntime2(params) {
                        // Note: we're mutating the parameter object here, normally a no-no, but shouldn't be
                        // an issue in our tests.
                        params.containerRuntimeCtor =
                            params.containerRuntimeCtor ?? MixinContainerRuntime;
                        params.containerScope = params.containerScope ?? params.context.scope;
                        return Base.loadRuntime2(params);
                    }
                    [methodName]() {
                        return methodReturn;
                    }
                };
                const myEntryPoint = {
                    myProp: "myValue",
                };
                const { runtime } = await makeMixin(makeMixin(containerRuntime_js_1.ContainerRuntime, "method1", "mixed in return"), "method2", 42).loadRuntime2({
                    context: getMockContext(),
                    provideEntryPoint: async (containerRuntime) => myEntryPoint,
                    existing: false,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                });
                node_assert_1.strict.equal(runtime.method1(), "mixed in return");
                node_assert_1.strict.equal(runtime.method2(), 42);
            });
            // A legacy partner team overrides the summarizeInternal method to add custom data to the Summary.
            // Let's make sure we don't break them inadvertently, while we work to move them to a better pattern.
            it("Ensure private member is stable to support legacy usage", async () => {
                const { runtime: containerRuntime_withSummarizeInternal_untyped } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const containerRuntime_withSummarizeInternal = containerRuntime_withSummarizeInternal_untyped;
                (0, node_assert_1.strict)(typeof containerRuntime_withSummarizeInternal.summarizeInternal === "function", "Expected summarizeInternal to be present (it's a private method)");
                const result = await containerRuntime_withSummarizeInternal.summarizeInternal(true, true);
                (0, node_assert_1.strict)(result.summary !== undefined, "Expected a valid result from summarizeInternal");
            });
        });
        describe("EntryPoint initialized correctly", () => {
            it("when using new loadRuntime2 method", async () => {
                const myEntryPoint = {
                    myProp: "myValue",
                };
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    provideEntryPoint: async (ctrRuntime) => myEntryPoint,
                    existing: false,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                });
                // The entryPoint should come from the provided initialization function.
                const actualEntryPoint = await containerRuntime.getEntryPoint();
                (0, node_assert_1.strict)(actualEntryPoint !== undefined, "entryPoint was not initialized");
                node_assert_1.strict.deepEqual(actualEntryPoint, myEntryPoint, "entryPoint does not match expected object");
            });
            it("loadRuntime2 accepts both requestHandlers and entryPoint", async () => {
                const myResponse = {
                    mimeType: "fluid/object",
                    value: "hello!",
                    status: 200,
                };
                const myEntryPoint = {
                    myProp: "myValue",
                };
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    requestHandler: async (req, ctrRuntime) => myResponse,
                    provideEntryPoint: async (ctrRuntime) => myEntryPoint,
                    existing: false,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                });
                // Calling request on the runtime should use the request handler we passed in the runtime's constructor.
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
                const responseFromRequestMethod = await containerRuntime.request({
                    url: "/",
                });
                node_assert_1.strict.deepEqual(responseFromRequestMethod, myResponse, "request method in runtime did not return the expected object");
                // The entryPoint should come from the provided initialization function.
                const actualEntryPoint = await containerRuntime.getEntryPoint();
                (0, node_assert_1.strict)(actualEntryPoint !== undefined, "entryPoint was not initialized");
                node_assert_1.strict.deepEqual(actualEntryPoint, myEntryPoint, "entryPoint does not match expected object");
            });
        });
        describe("Op content modification", () => {
            let containerRuntime;
            let pendingStateManager;
            beforeEach(async () => {
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
                pendingStateManager = containerRuntime.pendingStateManager;
            });
            it("modifying op content after submit does not reflect in PendingStateManager", () => {
                const content = { prop1: 1 };
                submitDataStoreOp(containerRuntime, "1", {
                    type: "op",
                    content: { address: "test", contents: content },
                });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                containerRuntime.flush();
                content.prop1 = 2;
                const state = pendingStateManager.getLocalState();
                node_assert_1.strict.notStrictEqual(state, undefined, "expect pending local state");
                node_assert_1.strict.strictEqual(state?.pending.pendingStates.length, 1, "expect 1 pending message");
                node_assert_1.strict.deepStrictEqual(
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                JSON.parse(state?.pending.pendingStates?.[0].content).contents.contents, {
                    type: "op",
                    content: { address: "test", contents: { prop1: 1 } },
                }, "content of pending local message has changed");
            });
        });
        describe("Container logging when loaded", () => {
            let mockLogger;
            const localGetMockContext = (featureGates = {}) => {
                return {
                    attachState: container_definitions_1.AttachState.Attached,
                    deltaManager: new internal_6.MockDeltaManager(),
                    audience: new internal_6.MockAudience(),
                    quorum: new internal_6.MockQuorumClients(),
                    taggedLogger: (0, internal_5.mixinMonitoringContext)(mockLogger, configProvider(featureGates)),
                    supportedFeatures: new Map([["referenceSequenceNumbers", true]]),
                    clientDetails: { capabilities: { interactive: true } },
                    closeFn: (_error) => { },
                    updateDirtyContainerState: (_dirty) => { },
                    getLoadedFromVersion: () => undefined,
                };
            };
            beforeEach(async () => {
                mockLogger = new internal_5.MockLogger();
            });
            const runtimeOptions = {
                compressionOptions: {
                    minimumBatchSizeInBytes: 1024 * 1024,
                    compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                },
                chunkSizeInBytes: 800 * 1024,
                enableGroupedBatching: true,
            };
            const defaultRuntimeOptions = {
                summaryOptions: {},
                gcOptions: {},
                loadSequenceNumberVerification: "close",
                flushMode: internal_3.FlushMode.TurnBased,
                compressionOptions: {
                    minimumBatchSizeInBytes: 614400,
                    compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                },
                maxBatchSizeInBytes: 700 * 1024,
                chunkSizeInBytes: 204800,
                enableRuntimeIdCompressor: undefined,
                enableGroupedBatching: true, // Redundant, but makes the JSON.stringify yield the same result as the logs
                explicitSchemaControl: false,
                createBlobPayloadPending: undefined,
                stagingModeAutoFlushThreshold: 1000,
                disableSchemaUpgrade: false,
            };
            const mergedRuntimeOptions = { ...defaultRuntimeOptions, ...runtimeOptions };
            it("Container load stats", async () => {
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: localGetMockContext({}),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                mockLogger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(mergedRuntimeOptions),
                        idCompressorMode: defaultRuntimeOptions.enableRuntimeIdCompressor,
                    },
                ]);
            });
            it("Container load stats with feature gate overrides", async () => {
                const featureGates = {
                    "Fluid.ContainerRuntime.IdCompressorEnabled": true,
                    "Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs": 1337,
                };
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: localGetMockContext(featureGates),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                mockLogger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(mergedRuntimeOptions),
                        idCompressorMode: "on",
                        featureGates: JSON.stringify({
                            closeSummarizerDelayOverride: 1337,
                        }),
                        groupedBatchingEnabled: true,
                    },
                ]);
            });
        });
        describe("Summarization", () => {
            let containerRuntime;
            async function yieldEventLoop() {
                const yieldP = new Promise((resolve) => {
                    setTimeout(resolve);
                });
                clock.tick(1);
                await yieldP;
            }
            beforeEach(async () => {
                const settings = {};
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(settings),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    provideEntryPoint: mockProvideEntryPoint,
                }));
            });
            it("summary is submitted successfully", async () => {
                const summarizeResult = await containerRuntime.submitSummary({
                    summaryLogger: (0, internal_5.createChildLogger)(),
                    cancellationToken: index_js_1.neverCancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                (0, node_assert_1.strict)(summarizeResult.stage === "submit", "Summary did not succeed");
            });
            it("summary fails if summary token is canceled", async () => {
                const cancelledSummaryToken = {
                    cancelled: true,
                    waitCancelled: new Promise(() => { }),
                };
                const summarizeResult = await containerRuntime.submitSummary({
                    summaryLogger: (0, internal_5.createChildLogger)(),
                    cancellationToken: cancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                (0, node_assert_1.strict)(summarizeResult.stage === "base", "Summary did not fail");
                node_assert_1.strict.strictEqual(summarizeResult.error?.message, "disconnected", "Summary was not canceled");
            });
            it("summary fails before generate if there are pending ops", async () => {
                // Submit an op and yield for it to be flushed from outbox to pending state manager.
                submitDataStoreOp(containerRuntime, "fakeId", {
                    type: "op",
                    content: { address: "fakeAddress", contents: "fakeContents" },
                });
                await yieldEventLoop();
                const summarizeResultP = containerRuntime.submitSummary({
                    summaryLogger: (0, internal_5.createChildLogger)(),
                    cancellationToken: index_js_1.neverCancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                // Advance the clock by the time that container runtime would wait for pending ops to be processed.
                clock.tick(containerRuntime_js_1.defaultPendingOpsWaitTimeoutMs);
                const summarizeResult = await summarizeResultP;
                (0, node_assert_1.strict)(summarizeResult.stage === "base", "Summary did not fail");
                node_assert_1.strict.strictEqual(summarizeResult.error?.message, "PendingOpsWhileSummarizing", "Summary did not fail with the right error");
                node_assert_1.strict.strictEqual((0, internal_5.isILoggingError)(summarizeResult.error) &&
                    summarizeResult.error.getTelemetryProperties().beforeGenerate, true, "It should have failed before generating summary");
            });
            it("summary fails after generate if there are pending ops", async () => {
                // Patch the summarize function to submit messages during it. This way there will be pending
                // messages after generating the summary.
                const patch = (fn) => {
                    const boundFn = fn.bind(containerRuntime);
                    return async (...args) => {
                        // Submit an op and yield for it to be flushed from outbox to pending state manager.
                        submitDataStoreOp(containerRuntime, "fakeId", {
                            type: "op",
                            content: { address: "fakeAddress", contents: "fakeContents" },
                        });
                        await yieldEventLoop();
                        return boundFn(...args);
                    };
                };
                containerRuntime.summarize = patch(containerRuntime.summarize);
                const summarizeResult = await containerRuntime.submitSummary({
                    summaryLogger: (0, internal_5.createChildLogger)(),
                    cancellationToken: index_js_1.neverCancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                (0, node_assert_1.strict)(summarizeResult.stage === "base", "Summary did not fail");
                node_assert_1.strict.strictEqual(summarizeResult.error?.message, "PendingOpsWhileSummarizing", "Summary did not fail with the right error");
                node_assert_1.strict.strictEqual((0, internal_5.isILoggingError)(summarizeResult.error) &&
                    summarizeResult.error.getTelemetryProperties().beforeGenerate, false, "It should have failed after generating summary");
            });
            it("summary passes if pending ops are processed during pending op processing timeout", async () => {
                const containerRuntimeWithSubmit = containerRuntime;
                // Submit a rejoin op and yield for it to be flushed from outbox to pending state manager.
                containerRuntimeWithSubmit.submit({
                    type: messageTypes_js_1.ContainerMessageType.Rejoin,
                    contents: undefined,
                }, undefined, undefined);
                await yieldEventLoop();
                // Create a mock logger to validate that pending ops event is generated with correct params.
                const mockLogger = new internal_5.MockLogger();
                const summaryLogger = (0, internal_5.createChildLogger)({ logger: mockLogger });
                const summarizeResultP = containerRuntime.submitSummary({
                    summaryLogger,
                    cancellationToken: index_js_1.neverCancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                // Advance the clock by 1 ms less than the time waited for pending ops to be processed. This will allow
                // summarization to proceed far enough to wait for pending ops.
                clock.tick(containerRuntime_js_1.defaultPendingOpsWaitTimeoutMs - 1);
                // Process the rejoin op so that there are no pending ops.
                containerRuntime.process({
                    type: "op",
                    clientId: "fakeClientId",
                    sequenceNumber: 0,
                    contents: {
                        type: messageTypes_js_1.ContainerMessageType.Rejoin,
                        contents: undefined,
                    },
                    minimumSequenceNumber: 0,
                }, true /* local */);
                // Advance the clock by the remaining time so that pending ops wait is completed.
                clock.tick(1);
                const summarizeResult = await summarizeResultP;
                (0, node_assert_1.strict)(summarizeResult.stage === "submit", "Summary did not succeed");
                mockLogger.assertMatch([
                    {
                        eventName: "PendingOpsWhileSummarizing",
                        countBefore: 1,
                        countAfter: 0,
                        saved: true,
                    },
                ]);
            });
        });
        describe("Snapshots", () => {
            /**
             * This test tests a scenario where a summarizer gets a newer summary ack, but on fetching the latest snapshot,
             * it gets a snapshot which is older than the one corresponding to the ack.
             * This can happen in cases such as database rollbacks in server which results in deleting recent snapshots but
             * not the corresponding acks.
             * Summarizers should not close in this scenario. They should continue generating summaries.
             */
            it("Summary succeeds on receiving summary ack for a deleted snapshot", async () => {
                // The latest snapshot version in storage.
                const latestVersion = {
                    id: "snapshot1",
                    treeId: "snapshotTree1",
                };
                // The latest snapshot tree in storage.
                const latestSnapshotTree = {
                    blobs: {},
                    trees: {
                        ".protocol": {
                            blobs: {
                                "attributes": "attributesBlob",
                            },
                            trees: {},
                        },
                    },
                };
                // The version of the snapshot that was deleted say during DB rollback.
                const deletedSnapshotId = "snapshot2";
                // The properties of the ack corresponding to the deleted snapshot.
                const deletedSnapshotAckOptions = {
                    proposalHandle: "proposal1",
                    ackHandle: deletedSnapshotId,
                    summaryRefSeq: 100,
                    summaryLogger: (0, internal_5.createChildLogger)({}),
                };
                class MockStorageService {
                    /**
                     * This always returns the same snapshot. Basically, when container runtime receives an ack for the
                     * deleted snapshot and tries to fetch the latest snapshot, return the latest snapshot.
                     */
                    async getSnapshotTree(version, scenarioName) {
                        node_assert_1.strict.strictEqual(version, latestVersion, "getSnapshotTree called with incorrect version");
                        return latestSnapshotTree;
                    }
                    async getVersions(
                    // eslint-disable-next-line @rushstack/no-new-null -- base signature uses `null`
                    versionId, count, scenarioName, fetchSource) {
                        return [latestVersion];
                    }
                    /**
                     * Validates that this is not called by container runtime with the deleted snapshot id even
                     * though it received an ack for it.
                     */
                    async uploadSummaryWithContext(summary, context) {
                        node_assert_1.strict.notStrictEqual(context.ackHandle, deletedSnapshotId, "Summary uploaded with deleted snapshot's ack");
                        return "snapshot3";
                    }
                    /**
                     * Called by container runtime to read document attributes. Return the sequence number as 0 which
                     * is lower than the deleted snapshot's reference sequence number.
                     */
                    async readBlob(id) {
                        node_assert_1.strict.strictEqual(id, "attributesBlob", "Not implemented");
                        const attributes = {
                            sequenceNumber: 0,
                            minimumSequenceNumber: 0,
                        };
                        return (0, client_utils_1.stringToBuffer)(JSON.stringify(attributes), "utf8");
                    }
                }
                const mockContext = getMockContext({
                    mockStorage: new MockStorageService(),
                    loadedFromVersion: latestVersion,
                });
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                // Call refresh latest summary with the deleted snapshot's options. Container runtime should
                // ignore this but not close.
                await node_assert_1.strict.doesNotReject(containerRuntime.refreshLatestSummaryAck(deletedSnapshotAckOptions), "Container runtime should not close");
                // Submit a summary. This should upload a summary with the snapshot container runtime loaded
                // from and not the deleted snapshot.
                const summarizeResult = await containerRuntime.submitSummary({
                    summaryLogger: (0, internal_5.createChildLogger)(),
                    cancellationToken: index_js_1.neverCancelledSummaryToken,
                    latestSummaryRefSeqNum: 0,
                });
                (0, node_assert_1.strict)(summarizeResult.stage === "submit", "Summary should not fail");
            });
        });
        describe("GetPendingState", () => {
            it("No Props. No pending state", async () => {
                const logger = new internal_5.MockLogger();
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const mockPendingStateManager = new Proxy({}, {
                    get: (_t, p, _r) => {
                        switch (p) {
                            case "getLocalState": {
                                return () => ({
                                    pending: { pendingStates: [] },
                                });
                            }
                            case "pendingMessagesCount": {
                                return 0;
                            }
                            default: {
                                node_assert_1.strict.fail(`unexpected access to pendingStateManager.${p}`);
                            }
                        }
                    },
                });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerRuntime.pendingStateManager = mockPendingStateManager;
                const state = containerRuntime.getPendingLocalState();
                node_assert_1.strict.ok(state.sessionExpiryTimerStarted !== undefined);
            });
            it("No Props. Some pending state", async () => {
                const logger = new internal_5.MockLogger();
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const pendingStates = Array.from({ length: 5 }, (_, i) => ({
                    content: i.toString(),
                    type: "message",
                    referenceSequenceNumber: 0,
                    localOpMetadata: undefined,
                    opMetadata: undefined,
                    batchInfo: { clientId: "CLIENT_ID", batchStartCsn: 1, length: 5, staged: false },
                    runtimeOp: undefined,
                }));
                const mockPendingStateManager = new Proxy({}, {
                    get: (_t, p, _r) => {
                        switch (p) {
                            case "getLocalState": {
                                return () => ({
                                    pending: { pendingStates },
                                });
                            }
                            case "pendingMessagesCount": {
                                return 5;
                            }
                            default: {
                                node_assert_1.strict.fail(`unexpected access to pendingStateManager.${p}`);
                            }
                        }
                    },
                });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerRuntime.pendingStateManager = mockPendingStateManager;
                const state = containerRuntime.getPendingLocalState();
                node_assert_1.strict.strictEqual(typeof state, "object");
                node_assert_1.strict.strictEqual(state.pending?.pendingStates, pendingStates);
            });
            it("sessionExpiryTimerStarted. No pending state", async () => {
                const logger = new internal_5.MockLogger();
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const state = (await containerRuntime.getPendingLocalState({
                    notifyImminentClosure: false,
                    sessionExpiryTimerStarted: 100,
                }));
                node_assert_1.strict.strictEqual(typeof state, "object");
                node_assert_1.strict.strictEqual(state.sessionExpiryTimerStarted, 100);
            });
            it("sessionExpiryTimerStarted. Some pending state", async () => {
                const logger = new internal_5.MockLogger();
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const pendingStates = Array.from({ length: 5 }, (_, i) => ({
                    content: i.toString(),
                    type: "message",
                    referenceSequenceNumber: 0,
                    localOpMetadata: undefined,
                    opMetadata: undefined,
                    batchInfo: { clientId: "CLIENT_ID", batchStartCsn: 1, length: 5, staged: false },
                    runtimeOp: undefined,
                }));
                const mockPendingStateManager = new Proxy({}, {
                    get: (_t, p, _r) => {
                        switch (p) {
                            case "getLocalState": {
                                return () => ({
                                    pending: { pendingStates },
                                });
                            }
                            case "pendingMessagesCount": {
                                return 5;
                            }
                            default: {
                                node_assert_1.strict.fail(`unexpected access to pendingStateManager.${p}`);
                            }
                        }
                    },
                });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerRuntime.pendingStateManager = mockPendingStateManager;
                const state = (await containerRuntime.getPendingLocalState({
                    notifyImminentClosure: false,
                    sessionExpiryTimerStarted: 100,
                }));
                node_assert_1.strict.strictEqual(state.sessionExpiryTimerStarted, 100);
            });
        });
        describe("Duplicate Batch Detection", () => {
            for (const enableBatchIdTracking of [undefined, true]) {
                it(`DuplicateBatchDetector is disabled if Batch Id Tracking isn't needed (${enableBatchIdTracking === true ? "ENABLED" : "DISABLED"})`, async () => {
                    const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext({
                            settings: {
                                "Fluid.ContainerRuntime.enableBatchIdTracking": enableBatchIdTracking,
                            },
                        }),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            enableRuntimeIdCompressor: "on",
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    // Process batch "batchId1" with seqNum 123
                    containerRuntime.process({
                        sequenceNumber: 123,
                        type: internal_2.MessageType.Operation,
                        contents: { type: messageTypes_js_1.ContainerMessageType.Rejoin, contents: undefined },
                        metadata: { batchId: "batchId1" },
                    }, false);
                    // Process a duplicate batch "batchId1" with different seqNum 234
                    const assertThrowsOnlyIfExpected = enableBatchIdTracking === true ? node_assert_1.strict.throws : node_assert_1.strict.doesNotThrow;
                    const errorPredicate = (e) => e.message === "Duplicate batch - The same batch was sequenced twice";
                    assertThrowsOnlyIfExpected(() => {
                        containerRuntime.process({
                            sequenceNumber: 234,
                            type: internal_2.MessageType.Operation,
                            contents: { type: messageTypes_js_1.ContainerMessageType.Rejoin, contents: undefined },
                            metadata: { batchId: "batchId1" },
                        }, false);
                    }, errorPredicate, "Expected duplicate batch detection to match Offline Load enablement");
                });
            }
            it("Can roundrip DuplicateBatchDetector state through summary/snapshot", async () => {
                // Duplicate Batch Detection requires OfflineLoad enabled
                const settings_enableOfflineLoad = {
                    "Fluid.ContainerRuntime.enableBatchIdTracking": true,
                };
                const { runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({
                        settings: settings_enableOfflineLoad,
                    }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                // Add batchId1 to DuplicateBatchDetected via ContainerRuntime.process,
                // and get its serialized representation from summarizing
                containerRuntime.process({
                    sequenceNumber: 123,
                    type: internal_2.MessageType.Operation,
                    contents: { type: messageTypes_js_1.ContainerMessageType.Rejoin, contents: undefined },
                    metadata: { batchId: "batchId1" },
                }, false);
                const { summary } = await containerRuntime.summarize({ fullTree: true });
                const blob = summary.tree[index_js_1.recentBatchInfoBlobName];
                (0, node_assert_1.strict)(blob?.type === internal_2.SummaryType.Blob, "Expected blob");
                node_assert_1.strict.equal(blob.content, '[[123,"batchId1"]]', "Expected single batchId mapping");
                // Load a new ContainerRuntime with the serialized DuplicateBatchDetector state.
                const mockStorage = {
                    // Hardcode readblob fn to return the blob contents put in the summary
                    readBlob: async (_id) => (0, client_utils_1.stringToBuffer)(blob.content, "utf8"),
                };
                const { runtime: containerRuntime2 } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({
                        settings: settings_enableOfflineLoad,
                        baseSnapshot: {
                            trees: {},
                            blobs: { [index_js_1.recentBatchInfoBlobName]: "nonempty_id_ignored_by_mockStorage" },
                        },
                        mockStorage,
                    }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                // Process an op with a duplicate batchId to what was loaded with
                node_assert_1.strict.throws(() => {
                    containerRuntime2.process({
                        sequenceNumber: 234,
                        type: internal_2.MessageType.Operation,
                        contents: { type: messageTypes_js_1.ContainerMessageType.Rejoin, contents: undefined },
                        metadata: { batchId: "batchId1" },
                    }, false);
                }, (e) => e.message === "Duplicate batch - The same batch was sequenced twice", "Expected duplicate batch detected after loading with recentBatchInfo");
            });
        });
        describe("Load Partial Snapshot with datastores with GroupId", () => {
            let snapshotWithContents;
            let blobContents;
            let ops;
            let containerRuntime;
            let containerContext;
            let entryDefault;
            let snapshotTree;
            let missingDataStoreRuntime;
            beforeEach(async () => {
                snapshotTree = {
                    id: "SnapshotId",
                    blobs: { ".metadata": "bARD4RKvW4LL1KmaUKp6hUMSp" },
                    trees: {
                        ".channels": {
                            blobs: {},
                            trees: {
                                default: {
                                    blobs: {
                                        ".component": "bARC6dCXlcrPxQHw3PeROtmKc",
                                    },
                                    trees: {
                                        ".channels": {
                                            blobs: {},
                                            trees: {
                                                root: { blobs: {}, trees: {} },
                                            },
                                        },
                                    },
                                },
                            },
                            unreferenced: true,
                        },
                        ".blobs": { blobs: {}, trees: {} },
                        "gc": {
                            id: "e8ed0760ac37fd8042020559779ce80b1d88f266",
                            blobs: {
                                __gc_root: "018d97818f8b519f99c418cb3c33ce5cc4e38e3f",
                            },
                            trees: {},
                        },
                    },
                };
                blobContents = new Map([
                    [
                        "bARD4RKvW4LL1KmaUKp6hUMSp",
                        (0, client_utils_1.stringToBuffer)(JSON.stringify({ summaryFormatVersion: 1, gcFeature: 3 }), "utf8"),
                    ],
                    [
                        "bARC6dCXlcrPxQHw3PeROtmKc",
                        (0, client_utils_1.stringToBuffer)(JSON.stringify({
                            pkg: '["@fluid-example/smde"]',
                            summaryFormatVersion: 2,
                            isRootDataStore: true,
                        }), "utf8"),
                    ],
                    [
                        "018d97818f8b519f99c418cb3c33ce5cc4e38e3f",
                        (0, client_utils_1.stringToBuffer)(JSON.parse(JSON.stringify('{"gcNodes":{"/":{"outboundRoutes":["/rootDOId"]},"/rootDOId":{"outboundRoutes":["/rootDOId/de68ca53-be31-479e-8d34-a267958997e4","/rootDOId/root"]},"/rootDOId/de68ca53-be31-479e-8d34-a267958997e4":{"outboundRoutes":["/rootDOId"]},"/rootDOId/root":{"outboundRoutes":["/rootDOId","/rootDOId/de68ca53-be31-479e-8d34-a267958997e4"]}}}')), "utf8"),
                    ],
                ]);
                ops = [
                    {
                        clientId: "X",
                        clientSequenceNumber: -1,
                        // eslint-disable-next-line unicorn/no-null
                        contents: null,
                        minimumSequenceNumber: 0,
                        referenceSequenceNumber: -1,
                        sequenceNumber: 1,
                        timestamp: 1623883807452,
                        type: "join",
                    },
                    {
                        clientId: "Y",
                        clientSequenceNumber: -1,
                        // eslint-disable-next-line unicorn/no-null
                        contents: null,
                        minimumSequenceNumber: 0,
                        referenceSequenceNumber: -1,
                        sequenceNumber: 2,
                        timestamp: 1623883811928,
                        type: "join",
                    },
                ];
                snapshotWithContents = {
                    blobContents,
                    ops,
                    snapshotTree,
                    sequenceNumber: 0,
                    snapshotFormatV: 1,
                    latestSequenceNumber: 2,
                };
                const logger = new internal_5.MockLogger();
                containerContext = getMockContext({ logger });
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- Modifying private property
                containerContext.snapshotWithContents = snapshotWithContents;
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- Modifying private property
                containerContext.baseSnapshot = snapshotWithContents.snapshotTree;
                containerContext.storage.readBlob = async (id) => {
                    return blobContents.get(id);
                };
                missingDataStoreRuntime = new internal_6.MockFluidDataStoreRuntime();
                const entryA = createDataStoreRegistryEntry([]);
                const entryB = createDataStoreRegistryEntry([]);
                entryDefault = createDataStoreRegistryEntry([
                    ["default", Promise.resolve(entryA)],
                    ["missingDataStore", Promise.resolve(entryB)],
                ]);
                logger.clear();
            });
            function createSnapshot(addMissingDatastore, setGroupId = true) {
                if (addMissingDatastore) {
                    // TODO: Fix this violation and remove the disable
                    // eslint-disable-next-line @fluid-internal/fluid/no-unchecked-record-access
                    snapshotTree.trees[".channels"].trees.missingDataStore = {
                        blobs: { ".component": "id" },
                        trees: {
                            ".channels": {
                                blobs: {},
                                trees: {
                                    root: { blobs: {}, trees: {} },
                                },
                            },
                        },
                        groupId: setGroupId ? "G1" : undefined,
                    };
                }
            }
            // Helper function that creates a FluidDataStoreRegistryEntry with the registry entries
            // provided to it.
            function createDataStoreRegistryEntry(entries) {
                const registryEntries = new Map(entries);
                const factory = {
                    type: "store-type",
                    get IFluidDataStoreFactory() {
                        return factory;
                    },
                    instantiateDataStore: async (context) => {
                        if (context.id === "missingDataStore") {
                            return missingDataStoreRuntime;
                        }
                        return new internal_6.MockFluidDataStoreRuntime();
                    },
                };
                const registry = {
                    get IFluidDataStoreRegistry() {
                        return registry;
                    },
                    // Returns the registry entry as per the entries provided in the param.
                    get: async (pkg) => registryEntries.get(pkg),
                };
                const entry = {
                    get IFluidDataStoreFactory() {
                        return factory;
                    },
                    get IFluidDataStoreRegistry() {
                        return registry;
                    },
                };
                return entry;
            }
            it("Load snapshot with missing snapshot contents for datastores should fail when groupId not specified", async () => {
                // In this test we will try to load the container runtime with a snapshot which has 2 datastores. However,
                // snapshot for datastore "missingDataStore" is omitted and we will check that the container runtime loads fine
                // but the "missingDataStore" is aliased, it fails if the snapshot for it does not have loadingGroupId to fetch
                // the omitted snapshot contents.
                createSnapshot(true /* addMissingDatastore */, false /* Don't set groupId property */);
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: containerContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([
                        ["@fluid-example/smde", Promise.resolve(entryDefault)],
                    ]),
                    existing: true,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                const defaultDataStore = await containerRuntime.getAliasedDataStoreEntryPoint("default");
                (0, node_assert_1.strict)(defaultDataStore !== undefined, "data store should load and is attached");
                await node_assert_1.strict.rejects(async () => {
                    await containerRuntime.getAliasedDataStoreEntryPoint("missingDataStore");
                }, "Resolving missing datastore should reject");
            });
            it("Load snapshot with missing snapshot contents for datastores should fail for summarizer in case group snapshot is ahead of initial snapshot seq number", async () => {
                // In this test we will try to load the container runtime with a snapshot which has 2 datastores. However,
                // snapshot for datastore "missingDataStore" is omitted and we will check that the container runtime loads fine
                // but the "missingDataStore" is requested/aliased, it fails to because for summarizer the fetched snapshot could
                // not be ahead of the base snapshot as that means that a snapshot is missing and the summarizer is not up to date.
                containerContext.storage.getSnapshot = async (snapshotFetchOptions) => {
                    snapshotWithContents.sequenceNumber = 10;
                    return snapshotWithContents;
                };
                createSnapshot(true /* addMissingDatastore */);
                containerContext.clientDetails.type = "summarizer";
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: containerContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([
                        ["@fluid-example/smde", Promise.resolve(entryDefault)],
                    ]),
                    existing: true,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                const defaultDataStore = await containerRuntime.getAliasedDataStoreEntryPoint("default");
                (0, node_assert_1.strict)(defaultDataStore !== undefined, "data store should load and is attached");
                await node_assert_1.strict.rejects(async () => {
                    await containerRuntime.getAliasedDataStoreEntryPoint("missingDataStore");
                }, (err) => {
                    (0, node_assert_1.strict)(err.message ===
                        "Summarizer client behind, loaded newer snapshot with loadingGroupId", "summarizer client is behind");
                    return true;
                });
            });
            it("Load snapshot with missing snapshot contents for datastores should load properly", async () => {
                // In this test we will try to load the container runtime with a snapshot which has 2 datastores. However,
                // snapshot for datastore "missingDataStore" is omitted and we will check that the container runtime loads fine
                // and when the "missingDataStore" is requested/aliased, it does that successfully.
                let getSnapshotCalledTimes = 0;
                containerContext.storage.getSnapshot = async (snapshotFetchOptions) => {
                    getSnapshotCalledTimes++;
                    snapshotWithContents.blobContents.set("id", (0, client_utils_1.stringToBuffer)(JSON.stringify({
                        pkg: '["@fluid-example/smde"]',
                        summaryFormatVersion: 2,
                        isRootDataStore: true,
                    }), "utf8"));
                    return snapshotWithContents;
                };
                createSnapshot(true /* addMissingDatastore */);
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: containerContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([
                        ["@fluid-example/smde", Promise.resolve(entryDefault)],
                    ]),
                    existing: true,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                const defaultDataStore = await containerRuntime.getAliasedDataStoreEntryPoint("default");
                (0, node_assert_1.strict)(defaultDataStore !== undefined, "data store should load and is attached");
                const datastore1 = await containerRuntime.resolveHandle({
                    url: "/missingDataStore",
                });
                // Mock Datastore runtime will return null when requested for "/".
                // eslint-disable-next-line unicorn/no-null
                node_assert_1.strict.strictEqual(datastore1, null, "resolveHandle should work fine");
                // Now try to get snapshot for missing data store again from container runtime. It should be returned
                // from cache.
                const snapshot = await containerRuntime.getSnapshotForLoadingGroupId(["G1"], ["missingDataStore"]);
                node_assert_1.strict.deepStrictEqual(snapshotTree.trees[".channels"]?.trees.missingDataStore, snapshot.snapshotTree, "snapshot should be equal");
                (0, node_assert_1.strict)(getSnapshotCalledTimes === 1, "second time should be from cache");
                // Set api to undefined to see that it should not be called again.
                // TODO: Fix this violation and remove the disable
                // eslint-disable-next-line require-atomic-updates
                containerContext.storage.getSnapshot = undefined;
                const datastore2 = await containerRuntime.resolveHandle({
                    url: "/missingDataStore",
                });
                (0, node_assert_1.strict)(datastore2 !== undefined, "resolveHandle should work fine");
            });
            it("Load snapshot with missing snapshot contents for datastores should work in case group snapshot is ahead of initial snapshot seq number", async () => {
                // In this test we will try to load the container runtime with a snapshot which has 2 datastores. However,
                // snapshot for datastore "missingDataStore" is omitted and we will check that the container runtime loads fine
                // and the container runtime waits for delta manager to reach snapshot seq number before returning the snapshot.
                containerContext.storage.getSnapshot = async (snapshotFetchOptions) => {
                    snapshotWithContents.blobContents.set("id", (0, client_utils_1.stringToBuffer)(JSON.stringify({
                        pkg: '["@fluid-example/smde"]',
                        summaryFormatVersion: 2,
                        isRootDataStore: true,
                    }), "utf8"));
                    snapshotWithContents.sequenceNumber = 5;
                    return snapshotWithContents;
                };
                createSnapshot(true /* addMissingDatastore */);
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: containerContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([
                        ["@fluid-example/smde", Promise.resolve(entryDefault)],
                    ]),
                    existing: true,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                const defaultDataStore = await containerRuntime.getAliasedDataStoreEntryPoint("default");
                (0, node_assert_1.strict)(defaultDataStore !== undefined, "data store should load and is attached");
                // Set it to seq number of partial fetched snapshot so that it is returned successfully by container runtime.
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerContext.deltaManager.lastSequenceNumber = 5;
                const missingDataStore = await containerRuntime.resolveHandle({
                    url: "/missingDataStore",
                });
                // Mock Datastore runtime will return null when requested for "/".
                // eslint-disable-next-line unicorn/no-null
                node_assert_1.strict.strictEqual(missingDataStore, null, "resolveHandle should work fine");
            });
            it("Load snapshot with missing snapshot contents for datastores should only process ops in datastore context which are after the snapshot seq number", async () => {
                // In this test we will try to load the container runtime with a snapshot which has 2 datastores. However,
                // snapshot for datastore "missingDataStore" is omitted and we will check that the container runtime loads fine
                // and the data store context only process ops which are after the snapshot seq number.
                containerContext.storage.getSnapshot = async (snapshotFetchOptions) => {
                    snapshotWithContents.sequenceNumber = 2;
                    snapshotWithContents.blobContents.set("id", (0, client_utils_1.stringToBuffer)(JSON.stringify({
                        pkg: '["@fluid-example/smde"]',
                        summaryFormatVersion: 2,
                        isRootDataStore: true,
                    }), "utf8"));
                    return snapshotWithContents;
                };
                createSnapshot(true /* addMissingDatastore */);
                ({ runtime: containerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: containerContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([
                        ["@fluid-example/smde", Promise.resolve(entryDefault)],
                    ]),
                    existing: true,
                    runtimeOptions: {
                        enableRuntimeIdCompressor: "on",
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                }));
                const defaultDataStore = await containerRuntime.getAliasedDataStoreEntryPoint("default");
                (0, node_assert_1.strict)(defaultDataStore !== undefined, "data store should load and is attached");
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                const missingDataStoreContext = 
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
                containerRuntime.channelCollection.contexts.get("missingDataStore");
                (0, node_assert_1.strict)(missingDataStoreContext !== undefined, "context should be there");
                const envelopes = [
                    { sequenceNumber: 1 },
                    { sequenceNumber: 2 },
                    { sequenceNumber: 3 },
                    { sequenceNumber: 4 },
                ];
                for (const envelope of envelopes) {
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
                    missingDataStoreContext.processMessages({
                        envelope,
                        messagesContent: [
                            { contents: "message", localOpMetadata: undefined, clientSequenceNumber: 1 },
                        ],
                        local: false,
                    });
                }
                // Set it to seq number of partial fetched snapshot so that it is returned successfully by container runtime.
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                containerContext.deltaManager.lastSequenceNumber = 2;
                let opsProcessed = 0;
                let opsStart;
                const processMessagesStub = (messageCollection) => {
                    if (opsProcessed === 0) {
                        opsStart = messageCollection.envelope.sequenceNumber;
                    }
                    opsProcessed += messageCollection.messagesContent.length;
                };
                const stub = sandbox
                    .stub(missingDataStoreRuntime, "processMessages")
                    .callsFake(processMessagesStub);
                await node_assert_1.strict.doesNotReject(async () => {
                    await containerRuntime.resolveHandle({ url: "/missingDataStore" });
                }, "resolveHandle should work fine");
                stub.restore();
                (0, node_assert_1.strict)(opsProcessed === 2, "only 2 ops should be processed with seq number 3 and 4");
                (0, node_assert_1.strict)(opsStart === 3, "first op processed should have seq number 3");
            });
        });
        it("Only log legacy codepath once", async () => {
            const mockLogger = new internal_5.MockLogger();
            let legacyLogger = (0, containerRuntime_js_1.getSingleUseLegacyLogCallback)((0, internal_5.createChildLogger)({ logger: mockLogger }), "someType");
            node_assert_1.strict.equal(mockLogger.events.length, 0, "Expected no event logged");
            legacyLogger = (0, containerRuntime_js_1.getSingleUseLegacyLogCallback)((0, internal_5.createChildLogger)({ logger: mockLogger }), "someType");
            legacyLogger("codePath1");
            mockLogger.assertMatch([{ eventName: "LegacyMessageFormat" }]);
            legacyLogger = (0, containerRuntime_js_1.getSingleUseLegacyLogCallback)((0, internal_5.createChildLogger)({ logger: mockLogger }), "someType");
            legacyLogger("codePath2");
            node_assert_1.strict.equal(mockLogger.events.length, 0, "Expected no more events logged");
        });
        describe("Signal Telemetry", () => {
            let containerRuntime;
            let logger;
            let droppedSignals;
            let runtimes;
            beforeEach(async () => {
                runtimes = new Map();
                logger = new internal_5.MockLogger();
                droppedSignals = [];
                const runtimeOptions = {
                    enableGroupedBatching: false,
                };
                const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    requestHandler: undefined,
                    runtimeOptions,
                    provideEntryPoint: mockProvideEntryPoint,
                });
                containerRuntime = runtime;
                // Assert that clientId is not undefined
                (0, node_assert_1.strict)(containerRuntime.clientId !== undefined, "clientId should not be undefined");
                runtimes.set(containerRuntime.clientId, containerRuntime);
                logger.clear();
            });
            function sendSignals(count) {
                for (let i = 0; i < count; i++) {
                    containerRuntime.submitSignal("TestSignalType", `TestSignalContent ${i + 1}`);
                    const { type, content } = submittedSignals[submittedSignals.length - 1].envelope.contents;
                    (0, node_assert_1.strict)(type === "TestSignalType", "Signal type should match");
                    assertSignalContentIsAString(content);
                    (0, node_assert_1.strict)(content === `TestSignalContent ${i + 1}`, "Signal content should match");
                }
            }
            function processSignals(signals, count) {
                const signalsToProcess = signals.splice(0, count);
                for (const signal of signalsToProcess) {
                    if (signal.targetClientId === undefined) {
                        for (const runtime of runtimes.values()) {
                            runtime.processSignal({
                                clientId: signal.clientId,
                                content: {
                                    clientBroadcastSignalSequenceNumber: signal.envelope.clientBroadcastSignalSequenceNumber,
                                    contents: signal.envelope.contents,
                                },
                                targetClientId: signal.targetClientId,
                            }, true);
                        }
                    }
                    else {
                        const runtime = runtimes.get(signal.targetClientId);
                        if (runtime) {
                            runtime.processSignal({
                                clientId: signal.clientId,
                                content: {
                                    clientBroadcastSignalSequenceNumber: signal.envelope.clientBroadcastSignalSequenceNumber,
                                    contents: signal.envelope.contents,
                                },
                                targetClientId: signal.targetClientId,
                            }, true);
                        }
                    }
                }
            }
            function processWithNoTargetSupport(count) {
                const signalsToProcess = submittedSignals.splice(0, count);
                for (const signal of signalsToProcess) {
                    for (const runtime of runtimes.values()) {
                        runtime.processSignal({
                            clientId: signal.clientId,
                            content: {
                                clientBroadcastSignalSequenceNumber: signal.envelope.clientBroadcastSignalSequenceNumber,
                                contents: signal.envelope.contents,
                            },
                        }, true);
                    }
                }
            }
            function processSubmittedSignals(count) {
                processSignals(submittedSignals, count);
            }
            function processDroppedSignals(count) {
                processSignals(droppedSignals, count);
            }
            function dropSignals(count) {
                const signalsToDrop = submittedSignals.splice(0, count);
                droppedSignals.push(...signalsToDrop);
            }
            it("emits signal latency telemetry after 100 signals", () => {
                // Send 1st signal and process it to prime the system
                sendSignals(1);
                processSubmittedSignals(1);
                // Send 100 more signals and process each of them in order
                sendSignals(100);
                processSubmittedSignals(100);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 1,
                        lost: 0,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 0,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                ], "Signal latency telemetry should be logged after 100 signals", 
                /* inlineDetailsProp = */ true);
            });
            it("emits SignalLost error event when signal is dropped", () => {
                sendSignals(4);
                processSubmittedSignals(1);
                dropSignals(2);
                processSubmittedSignals(1);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 2,
                        expectedSequenceNumber: 2,
                        clientBroadcastSignalSequenceNumber: 4,
                    },
                ], "SignalLost telemetry should be logged when signal is dropped", 
                /* inlineDetailsProp = */ true);
            });
            it("emits SignalOutOfOrder error event when missing signal is received non-sequentially", () => {
                sendSignals(3);
                processSubmittedSignals(1);
                dropSignals(1);
                processSubmittedSignals(1);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 1,
                        expectedSequenceNumber: 2,
                        clientBroadcastSignalSequenceNumber: 3,
                    },
                ], "SignalLost telemetry should be logged when signal is dropped", 
                /* inlineDetailsProp = */ true, false);
                logger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                ], "SignalOutOfOrder telemetry should not be logged on lost signal");
                processDroppedSignals(1);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                ], "SignalOutOfOrder telemetry should be logged when missing signal is received non-sequentially");
            });
            it("does not emit error events when signals are processed in order", () => {
                sendSignals(100);
                processSubmittedSignals(100);
                logger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                    },
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                ], "SignalLost and SignalOutOfOrder telemetry should not be logged when signals are processed in order");
            });
            it("logs relative lost signal count in SignalLost telemetry", () => {
                sendSignals(5);
                dropSignals(1);
                processSubmittedSignals(1);
                // Missing signal should be detected
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 1,
                        expectedSequenceNumber: 1,
                        clientBroadcastSignalSequenceNumber: 2,
                    },
                ], "SignalLost telemetry should be logged when signal is dropped", 
                /* inlineDetailsProp = */ true);
                dropSignals(2);
                processSubmittedSignals(1);
                // Missing 3rd and 4th signals should be detected
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 2,
                        expectedSequenceNumber: 3,
                        clientBroadcastSignalSequenceNumber: 5,
                    },
                ], "SignalLost telemetry should be logged when signal is dropped", 
                /* inlineDetailsProp = */ true);
            });
            it("ignores signals sent before disconnect and resets stats on reconnect", () => {
                // Define resubmit and setConnectionState on channel collection
                // This is needed to submit test data store ops
                stubChannelCollection(containerRuntime);
                sendSignals(4);
                // Submit op so message is queued in PendingStateManager
                // This is needed to increase reconnect count
                submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                // Disconnect + Reconnect
                changeConnectionState(containerRuntime, false, mockClientId);
                changeConnectionState(containerRuntime, true, mockClientId);
                // Temporarily lose two old signals
                dropSignals(2);
                // Receive old signals sent before disconnect
                processSubmittedSignals(2);
                // Receive old out of order signals
                processDroppedSignals(2);
                // No error events should be logged for signals sent before disconnect
                logger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                    {
                        eventName: "ContainerRuntime:SignalLost",
                    },
                ], "SignalOutOfOrder/SignalLost telemetry should not be logged on reconnect");
                sendSignals(100);
                processSubmittedSignals(100);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 97, // 101 (tracked latency signal) - 5 (earliest sent signal on reconnect) + 1 = 97
                        lost: 0,
                        outOfOrder: 0,
                        reconnectCount: 1,
                    },
                ], "SignalLatency telemetry should be logged with correct reconnect count", 
                /* inlineDetailsProp = */ true);
            });
            it("ignores signals sent while disconnected and resets stats on reconnect", () => {
                // SETUP - define resubmit and setConnectionState on channel collection.
                // This is needed to submit test data store ops. Once defined, submit a test data store op
                // so that message is queued in PendingStateManager and reconnect count is increased.
                stubChannelCollection(containerRuntime);
                // Send and process an initial signal to prime the system.
                submitDataStoreOp(containerRuntime, "1", testDataStoreMessage);
                sendSignals(1); // 1st signal (#1)
                processSubmittedSignals(1);
                // ACT - Disconnect client and send signals while disconnected.
                // Reconnect client and continue sending signals.
                changeConnectionState(containerRuntime, false, mockClientId);
                // Send and drop 150 signals (#2 to #151)
                sendSignals(150);
                dropSignals(150);
                changeConnectionState(containerRuntime, true, mockClientId);
                // Send and process 100 signals (#152 to #251)
                // This should include tracked latency signal (#251)
                sendSignals(100);
                processSubmittedSignals(100);
                // VERIFY - SignalLatency telemetry should be logged with correct reconnect count
                // No error events should be logged for signals sent before disconnect
                logger.assertMatchNone([
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                    {
                        eventName: "ContainerRuntime:SignalLost",
                    },
                ], "SignalOutOfOrder/SignalLost telemetry should not be logged on reconnect", 
                /* inlineDetailsProp = */ true, 
                /* clearEventsAfterCheck = */ false);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 50, // 201 (tracked latency signal) - 152 (earliest sent signal on reconnect) + 1 = 50
                        lost: 0,
                        outOfOrder: 0,
                        reconnectCount: 1,
                    },
                ], "SignalLatency telemetry should be logged with correct reconnect count", 
                /* inlineDetailsProp = */ true);
            });
            it("counts both relative and absolute lost signal counts", () => {
                sendSignals(60);
                processSubmittedSignals(10);
                dropSignals(1);
                processSubmittedSignals(39);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 1,
                        expectedSequenceNumber: 11,
                        clientBroadcastSignalSequenceNumber: 12,
                    },
                ], "SignalLost telemetry should log relative lost signal count when a signal is dropped", 
                /* inlineDetailsProp = */ true);
                dropSignals(5);
                sendSignals(45);
                processSubmittedSignals(30);
                dropSignals(4);
                // Process remaining signals
                processSubmittedSignals(16);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 5,
                        expectedSequenceNumber: 51,
                        clientBroadcastSignalSequenceNumber: 56,
                    },
                    {
                        eventName: "ContainerRuntime:SignalLost",
                        signalsLost: 4,
                        expectedSequenceNumber: 86,
                        clientBroadcastSignalSequenceNumber: 90,
                    },
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 10,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                ], "SignalLost telemetry should log relative lost signal count and SignalLatency telemetry should log absolute lost signal count for each batch of 100 signals", 
                /* inlineDetailsProp = */ true);
            });
            it("accurately reports amount of sent and lost signals with multiple SignalLatency events", () => {
                // Send 50 signals and drop 10
                sendSignals(50);
                dropSignals(10);
                processSubmittedSignals(40);
                // Send 60 signals and drop 10
                sendSignals(60);
                processSubmittedSignals(40);
                dropSignals(10);
                // Here we should detect that 100 signals have been sent and 20 signals were lost
                processSubmittedSignals(10);
                // Send 100 signals and drop 1
                sendSignals(100);
                dropSignals(1);
                // Here we should detect that 100 more signals have been sent and 1 signal was lost
                processSubmittedSignals(99);
                // Check SignalLatency logs amount of sent and lost signals
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 101,
                        lost: 20,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 1,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                ], "SignalLatency telemetry should log absolute lost signal count for each batch of 100 signals", 
                /* inlineDetailsProp = */ true);
            });
            it("accurately reports amount of sent and lost signals when roundtrip tracked signal is dropped", () => {
                // Send 50 signals and drop 10
                sendSignals(50);
                dropSignals(10);
                processSubmittedSignals(40);
                // Send 60 signals and drop 15 (including roundtrip tracked signal)
                sendSignals(60);
                processSubmittedSignals(40);
                dropSignals(15); // Drop roundtrip tracked signal
                // Since roundtrip signal is lost, we don't expect to see SignalLatency telemetry for the first 100 signals
                processSubmittedSignals(5);
                // Send 100 signals and drop 1
                sendSignals(100);
                dropSignals(1);
                // Here we should detect that 200 signals have been sent and 26 signals were lost
                processSubmittedSignals(99);
                // Check SignalLatency logs amount of sent and lost signals
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 201,
                        lost: 26,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                ], "SignalLatency telemetry should log absolute lost signal count for each batch of 100 signals", 
                /* inlineDetailsProp = */ true);
            });
            it("accurately reports amount of sent and lost signals when rapid fire more than 100+ signals", () => {
                sendSignals(1);
                processSubmittedSignals(1);
                sendSignals(101);
                dropSignals(10);
                processSubmittedSignals(91);
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 10,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                /* inlineDetailsProp = */ true);
            });
            it("should log out of order signal in between signal latency events", () => {
                // Send 1st signal and process it to prime the system
                sendSignals(1);
                processSubmittedSignals(1);
                // Send 150 signals and temporarily lose 1
                sendSignals(150); //           150 outstanding including 1 tracked signal (#101); max #151
                processSubmittedSignals(95); // 55 outstanding including 1 tracked signal (#101)
                dropSignals(1); //              54 outstanding including 1 tracked signal (#101)
                processSubmittedSignals(14); // 40 outstanding; none tracked
                processDroppedSignals(1); //    40 outstanding; none tracked *out of order signal*
                processSubmittedSignals(40); //  0 outstanding; none tracked
                // Send 60 signals including tracked signal
                sendSignals(60); //             60 outstanding including 1 tracked signal (#201); max #211
                processSubmittedSignals(60); //  0 outstanding; none tracked
                // Check SignalLatency logs amount of sent and lost signals
                logger.assertMatch([
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 1,
                        outOfOrder: 0,
                        reconnectCount: 0,
                    },
                    {
                        eventName: "ContainerRuntime:SignalOutOfOrder",
                    },
                    {
                        eventName: "ContainerRuntime:SignalLatency",
                        sent: 100,
                        lost: 0,
                        outOfOrder: 1,
                        reconnectCount: 0,
                    },
                ], "SignalLatency telemetry should log absolute lost signal count for each batch of 100 signals and SignalOutOfOrder event", 
                /* inlineDetailsProp = */ true);
            });
            describe("multi-client", () => {
                let remoteContainerRuntime;
                let remoteLogger;
                function sendRemoteSignals(count) {
                    for (let i = 0; i < count; i++) {
                        remoteContainerRuntime.submitSignal("TestSignalType", `TestSignalContent ${i + 1}`);
                    }
                }
                beforeEach(async () => {
                    remoteLogger = new internal_5.MockLogger();
                    const runtimeOptions = {
                        enableGroupedBatching: false,
                    };
                    ({ runtime: remoteContainerRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext({ logger: remoteLogger }, "remoteMockClientId"),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        requestHandler: undefined,
                        runtimeOptions,
                        provideEntryPoint: mockProvideEntryPoint,
                    }));
                    // Assert that clientId is not undefined
                    (0, node_assert_1.strict)(remoteContainerRuntime.clientId !== undefined, "clientId should not be undefined");
                    runtimes.set(remoteContainerRuntime.clientId, remoteContainerRuntime);
                });
                it("ignores remote targeted signal in signalLatency telemetry", () => {
                    // Send 1st signal and process it to prime the system
                    sendSignals(1);
                    processSubmittedSignals(1);
                    // Send 101 signals (one targeted)
                    sendSignals(50); //             50 outstanding; none tracked;
                    containerRuntime.submitSignal("TargetedSignalType", "TargetedSignalContent", remoteContainerRuntime.clientId); //                           51 outstanding; none tracked; one remote targeted
                    sendSignals(49); //            100 outstanding including 1 tracked signals (#101); one targeted
                    processSubmittedSignals(100); // 0 outstanding; none tracked
                    // Check that remote targeted signal is ignored
                    logger.assertMatchNone([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                    sendSignals(1); //               1 outstanding including 1 tracked signals (#101); one targeted
                    processSubmittedSignals(1); //   0 outstanding; none tracked
                    // Check for logged SignalLatency event
                    logger.assertMatch([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                    // Repeat the same for remote runtime which recevied targeted signal
                    sendRemoteSignals(1);
                    processSubmittedSignals(1);
                    sendRemoteSignals(99);
                    processSubmittedSignals(99);
                    remoteLogger.assertMatchNone([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                    sendRemoteSignals(1);
                    processSubmittedSignals(1);
                    // Check for logged SignalLatency event
                    remoteLogger.assertMatch([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                });
                it("can detect dropped signal while ignoring non-self targeted signal in signalLatency telemetry", () => {
                    // Send 1st signal and process it to prime the system
                    sendSignals(1);
                    processSubmittedSignals(1);
                    // Send 100 signals (one targeted) and drop 10
                    sendSignals(40); //              40 outstanding; none tracked;
                    containerRuntime.submitSignal("TargetedSignalType", "TargetedSignalContent", remoteContainerRuntime.clientId); //                            41 outstanding; none tracked; one remote targeted
                    sendSignals(40); //              81 outstanding; none tracked; one remote targeted
                    dropSignals(10); //              71 outstanding; none tracked; one remote targeted
                    sendSignals(20); //              91 outstanding; none tracked; one remote targeted
                    // Process all signals (5 out of order)
                    processSubmittedSignals(85); //   6 outstanding; none tracked;
                    processDroppedSignals(5); //      6 outstanding; none tracked; *out of order signals*
                    processSubmittedSignals(6); //    0 outstanding; none tracked;
                    // Check for logged SignalLatency event
                    logger.assertMatch([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 10,
                            outOfOrder: 5,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                });
                it("ignores unexpected targeted signal for a remote client", () => {
                    // Send 1st signal and process it to prime the system
                    sendSignals(1);
                    processSubmittedSignals(1);
                    // Send 101 signals (one targeted)
                    sendSignals(50); //                50 outstanding; none tracked;
                    containerRuntime.submitSignal("TargetedSignalType", "TargetedSignalContent", remoteContainerRuntime.clientId); //                              51 outstanding; none tracked; one remote targeted
                    sendSignals(49); //               100 outstanding; none tracked; one remote targeted
                    processWithNoTargetSupport(100); // 0 outstanding; none tracked
                    // Check that 'targeted signal' is ignored
                    logger.assertMatchNone([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                    sendSignals(1); //             	     1 outstanding including 1 tracked signals (#101); one targeted
                    processWithNoTargetSupport(1); //    0 outstanding; none tracked
                    // Check for logged SignalLatency event
                    logger.assertMatch([
                        {
                            eventName: "ContainerRuntime:SignalLatency",
                            sent: 100,
                            lost: 0,
                            outOfOrder: 0,
                            reconnectCount: 0,
                        },
                    ], "SignalLatency telemetry should log correct amount of sent and lost signals", 
                    /* inlineDetailsProp = */ true);
                });
            });
            describe("Validate runtime options", () => {
                it("Throws when op compression is on and op grouping is off", async () => {
                    await node_assert_1.strict.rejects(async () => containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext(),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            enableGroupedBatching: false,
                            compressionOptions: {
                                compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                                minimumBatchSizeInBytes: 1,
                            },
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    }), (error) => {
                        return (error.errorType === internal_1.ContainerErrorTypes.usageError &&
                            error.message === "If compression is enabled, op grouping must be enabled too");
                    }, "Container should throw when op compression is on and op grouping is off");
                });
                it("Throws when createBlobPayloadPending is on and explicitSchemaControl is not enabled", async () => {
                    await node_assert_1.strict.rejects(async () => containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext(),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            createBlobPayloadPending: true,
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    }), (error) => {
                        return (error.errorType === internal_1.ContainerErrorTypes.usageError &&
                            error.message ===
                                "explicitSchemaControl must be enabled to use createBlobPayloadPending");
                    }, "Container should throw when createBlobPayloadPending is on and explicitSchemaControl is not enabled");
                });
            });
        });
        describe("Default Configurations", () => {
            it("minVersionForCollab not provided", async () => {
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.TurnBased,
                    compressionOptions: {
                        minimumBatchSizeInBytes: 614400,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: true,
                    explicitSchemaControl: false,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab: internal_4.defaultMinVersionForCollab,
                    },
                ]);
            });
            // These are examples of minVersionForCollab inputs that are not valid.
            // minVersionForCollab should be at least 1.0.0 and less than or equal to
            // the current pkgVersion.
            const invalidVersions = ["0.50.0", "100.0.0"];
            for (const version of invalidVersions) {
                it(`throws when minVersionForCollab = ${version}`, async () => {
                    const logger = new internal_5.MockLogger();
                    await node_assert_1.strict.rejects(async () => {
                        await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                            context: getMockContext({ logger }),
                            registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                            existing: false,
                            runtimeOptions: {},
                            provideEntryPoint: mockProvideEntryPoint,
                            // @ts-expect-error - Invalid version strings are not castable to MinimumVersionForCollab
                            minVersionForCollab: version,
                        });
                    });
                });
            }
            it("minVersionForCollab = 1.0.0", async () => {
                const minVersionForCollab = "1.0.0";
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                    minVersionForCollab,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.Immediate,
                    compressionOptions: {
                        minimumBatchSizeInBytes: Number.POSITIVE_INFINITY,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: false,
                    explicitSchemaControl: false,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab,
                    },
                ]);
            });
            it('minVersionForCollab = 2.0.0-defaults ("default")', async () => {
                const minVersionForCollab = "2.0.0-defaults";
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                    minVersionForCollab,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.TurnBased,
                    compressionOptions: {
                        minimumBatchSizeInBytes: 614400,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: true,
                    explicitSchemaControl: false,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab: internal_4.defaultMinVersionForCollab,
                    },
                ]);
            });
            it("minVersionForCollab = 2.0.0 (explicit)", async () => {
                const minVersionForCollab = "2.0.0";
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                    minVersionForCollab,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.TurnBased,
                    compressionOptions: {
                        minimumBatchSizeInBytes: 614400,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: true,
                    explicitSchemaControl: true,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab,
                    },
                ]);
            });
            it("minVersionForCollab = 2.20.0", async () => {
                const minVersionForCollab = "2.20.0";
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    provideEntryPoint: mockProvideEntryPoint,
                    minVersionForCollab,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.TurnBased,
                    compressionOptions: {
                        minimumBatchSizeInBytes: 614400,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: true,
                    explicitSchemaControl: true,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab,
                    },
                ]);
            });
            it("minVersionForCollab not provided, with manual configs", async () => {
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        summaryOptions: { initialSummarizerDelayMs: 1 },
                        gcOptions: { enableGCSweep: true },
                        loadSequenceNumberVerification: "bypass",
                        flushMode: internal_3.FlushMode.Immediate,
                        maxBatchSizeInBytes: 100,
                        chunkSizeInBytes: 200,
                        enableRuntimeIdCompressor: "on",
                        enableGroupedBatching: false, // By turning off batching, we will also disable compression automatically
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: { initialSummarizerDelayMs: 1 },
                    gcOptions: { enableGCSweep: true },
                    loadSequenceNumberVerification: "bypass",
                    flushMode: internal_3.FlushMode.Immediate,
                    compressionOptions: {
                        minimumBatchSizeInBytes: Number.POSITIVE_INFINITY,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 100,
                    chunkSizeInBytes: 200,
                    enableRuntimeIdCompressor: "on",
                    enableGroupedBatching: false,
                    explicitSchemaControl: false,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab: internal_4.defaultMinVersionForCollab,
                    },
                ]);
            });
            for (const { desc, runtimeOptions } of [
                { desc: "all options unspecified", runtimeOptions: {} },
                {
                    // This case tests degenerate specification that is permissible
                    // with exactOptionalPropertyTypes disabled. Even when enabled,
                    // this should be tested in case callers violate exactness.
                    // (use @ts-expect-error or `as` to ignore when needed)
                    desc: "all options explicitly undefined",
                    runtimeOptions: {
                        summaryOptions: undefined,
                        gcOptions: undefined,
                        loadSequenceNumberVerification: undefined,
                        flushMode: undefined,
                        maxBatchSizeInBytes: undefined,
                        chunkSizeInBytes: undefined,
                        enableRuntimeIdCompressor: undefined,
                        enableGroupedBatching: undefined,
                        compressionOptions: undefined,
                        explicitSchemaControl: undefined,
                        disableSchemaUpgrade: undefined,
                        createBlobPayloadPending: undefined,
                    },
                },
            ])
                it(desc, async () => {
                    const logger = new internal_5.MockLogger();
                    await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext({ logger }),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions,
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    const expectedRuntimeOptions = {
                        summaryOptions: {},
                        gcOptions: {},
                        loadSequenceNumberVerification: "close",
                        flushMode: internal_3.FlushMode.TurnBased,
                        compressionOptions: {
                            minimumBatchSizeInBytes: 614400,
                            compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                        },
                        maxBatchSizeInBytes: 716800,
                        chunkSizeInBytes: 204800,
                        enableRuntimeIdCompressor: undefined, // idCompressor is undefined, since that represents a logical state (off)
                        enableGroupedBatching: true,
                        explicitSchemaControl: false,
                        stagingModeAutoFlushThreshold: 1000,
                        disableSchemaUpgrade: false,
                    };
                    logger.assertMatchAny([
                        {
                            eventName: "ContainerRuntime:ContainerLoadStats",
                            category: "generic",
                            options: JSON.stringify(expectedRuntimeOptions),
                            minVersionForCollab: internal_4.defaultMinVersionForCollab,
                        },
                    ]);
                });
            // Note: We may need to update `expectedRuntimeOptions` for this test
            // when we bump to certain versions.
            it("minVersionForCollab = pkgVersion", async () => {
                const minVersionForCollab = packageVersion_js_1.pkgVersion;
                const logger = new internal_5.MockLogger();
                await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext({ logger }),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                    minVersionForCollab,
                });
                const expectedRuntimeOptions = {
                    summaryOptions: {},
                    gcOptions: {},
                    loadSequenceNumberVerification: "close",
                    flushMode: internal_3.FlushMode.TurnBased,
                    compressionOptions: {
                        minimumBatchSizeInBytes: 614400,
                        compressionAlgorithm: compressionDefinitions_js_1.CompressionAlgorithms.lz4,
                    },
                    maxBatchSizeInBytes: 716800,
                    chunkSizeInBytes: 204800,
                    enableRuntimeIdCompressor: undefined,
                    enableGroupedBatching: true,
                    explicitSchemaControl: true,
                    stagingModeAutoFlushThreshold: 1000,
                    disableSchemaUpgrade: false,
                };
                logger.assertMatchAny([
                    {
                        eventName: "ContainerRuntime:ContainerLoadStats",
                        category: "generic",
                        options: JSON.stringify(expectedRuntimeOptions),
                        minVersionForCollab,
                    },
                ]);
            });
            for (const runtimeOption of [
                { enableGroupedBatching: true },
                { enableGroupedBatching: true, compressionOptions: compressionDefinitions_js_1.enabledCompressionConfig },
                { explicitSchemaControl: true },
                { gcOptions: { enableGCSweep: true } },
                // Adding in an arbitrary entry into the IGCRuntimeOptions object
                { gcOptions: { enableGCSweep: true, sweepGracePeriodMs: 1 } },
                { enableRuntimeIdCompressor: "on" },
                { enableRuntimeIdCompressor: "delayed" },
                { createBlobPayloadPending: true },
                { flushMode: internal_3.FlushMode.TurnBased },
            ]) {
                it(`throws if minVersionForCollab is incompatible with runtimeOptions: ${JSON.stringify(runtimeOption)}`, async () => {
                    const runtimeOptions = {
                        ...runtimeOption,
                    };
                    const logger = new internal_5.MockLogger();
                    const minVersionForCollab = "1.0.0";
                    await node_assert_1.strict.rejects(async () => {
                        await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                            context: getMockContext({ logger }),
                            registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                            existing: false,
                            runtimeOptions,
                            provideEntryPoint: mockProvideEntryPoint,
                            minVersionForCollab,
                        });
                    });
                });
            }
            it("does not throw if minVersionForCollab is not set and the default is incompatible with runtimeOptions", async () => {
                const logger = new internal_5.MockLogger();
                await node_assert_1.strict.doesNotReject(async () => {
                    await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: getMockContext({ logger }),
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        // We would normally throw (since `createBlobPayloadPending` requires 2.40), but since we did
                        // not explicity set minVersionForCollab, we allow it.
                        runtimeOptions: { createBlobPayloadPending: true, explicitSchemaControl: true },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                });
            });
        });
        describe("Staging Mode", () => {
            let containerRuntime;
            beforeEach("init", async () => {
                const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: getMockContext(),
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                containerRuntime = runtime;
                submittedOps.length = 0; // reuse array defined higher in this file
            });
            afterEach("dispose", () => {
                containerRuntime.dispose();
                containerRuntime = undefined;
            });
            it("entering and exiting updates inStagingMode flag", () => {
                (0, node_assert_1.strict)(containerRuntime.attachState === container_definitions_1.AttachState.Attached, "PRECONDITION: Expected Attached container");
                const controls = containerRuntime.enterStagingMode();
                node_assert_1.strict.equal(containerRuntime.inStagingMode, true, "Runtime should be in staging mode after entry");
                controls.commitChanges();
                node_assert_1.strict.equal(containerRuntime.inStagingMode, false, "Runtime should exit staging mode after commit");
                // Enter / discard as a second exit-path
                containerRuntime.enterStagingMode().discardChanges();
                node_assert_1.strict.equal(containerRuntime.inStagingMode, false, "Runtime should exit staging mode after discard");
            });
            it("entering staging mode twice not allowed", () => {
                const controls = containerRuntime.enterStagingMode();
                node_assert_1.strict.throws(() => containerRuntime.enterStagingMode(), (error) => error.errorType === internal_1.ContainerErrorTypes.usageError &&
                    error.message === "Already in staging mode", "Should not allow entering staging mode while already in staging mode");
                controls.discardChanges();
                // Now we can enter staging mode again
                containerRuntime.enterStagingMode();
                node_assert_1.strict.equal(containerRuntime.inStagingMode, true, "Runtime should be in staging mode after re-entry");
            });
            it("can enter staging mode while attaching", async () => {
                const mockContext = getMockContext({
                    attachState: container_definitions_1.AttachState.Attaching,
                });
                const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                containerRuntime = runtime;
                node_assert_1.strict.doesNotThrow(() => containerRuntime.enterStagingMode(), "Should allow entering staging mode while Attaching");
            });
            it("cannot enter staging mode while detached", async () => {
                const mockContext = getMockContext({
                    attachState: container_definitions_1.AttachState.Detached,
                });
                const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context: mockContext,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {},
                    provideEntryPoint: mockProvideEntryPoint,
                });
                containerRuntime = runtime;
                node_assert_1.strict.throws(() => containerRuntime.enterStagingMode(), (error) => error.errorType === internal_1.ContainerErrorTypes.usageError &&
                    error.message === "Cannot enter staging mode while Detached", "Should not allow entering staging mode while Detached");
            });
            it("commitChanges submits staged ops", () => {
                const channelCollectionStub = stubChannelCollection(containerRuntime);
                // Won't be resubmitted when exiting staging mode
                submitDataStoreOp(containerRuntime, "1", genTestDataStoreMessage("pre-staging"));
                const controls = containerRuntime.enterStagingMode();
                (0, node_assert_1.strict)(channelCollectionStub.notifyStagingMode.calledOnceWithExactly(true), "Expected notifyStagingMode to be called with true");
                // Entering staging mode triggers a flush, so we should see the pre-staging op sent,
                // but not the staged op (even with flush)
                node_assert_1.strict.equal(submittedOps.length, 1, "Pre-staging op expected to be submitted");
                const stagedOpContents = genTestDataStoreMessage("staged-op");
                submitDataStoreOp(containerRuntime, "2", stagedOpContents, "LOCAL_OP_METADATA");
                containerRuntime.flush();
                node_assert_1.strict.equal(submittedOps.length, 1, "Only expected the 1 pre-staging op");
                // default options: { squash: false }
                controls.commitChanges();
                (0, node_assert_1.strict)(channelCollectionStub.reSubmitContainerMessage.calledOnce, "Expected reSubmit to be called once. Prestaging op should not be resubmitted");
                (0, node_assert_1.strict)(channelCollectionStub.reSubmitContainerMessage.calledWithExactly({
                    type: messageTypes_js_1.ContainerMessageType.FluidDataStoreOp,
                    contents: { address: "2", contents: stagedOpContents },
                }, "LOCAL_OP_METADATA", 
                /* squash: */ false), "Unexpected args for reSubmit");
                (0, node_assert_1.strict)(channelCollectionStub.notifyStagingMode.getCall(1)?.calledWithExactly(false), "Expected notifyStagingMode to be called with false on the second call");
                node_assert_1.strict.equal(submittedOps.length, 2, "Staged op should be resubmitted after commitChanges");
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                node_assert_1.strict.equal(submittedOps[1].contents.address, "2", "Unexpected staged op address");
            });
            it("discardChanges drops staged ops", () => {
                const channelCollectionStub = stubChannelCollection(containerRuntime);
                // Won't be rolled back when exiting staging mode
                submitDataStoreOp(containerRuntime, "1", genTestDataStoreMessage("pre-staging"), "LOCAL_OP_METADATA");
                const controls = containerRuntime.enterStagingMode();
                // Entering staging mode triggers a flush, so we should see the pre-staging op sent,
                // but not the staged op (even with flush)
                node_assert_1.strict.equal(submittedOps.length, 1, "Pre-staging op expected to be submitted");
                const stagedOpContents = genTestDataStoreMessage("staged-op");
                submitDataStoreOp(containerRuntime, "2", stagedOpContents, "LOCAL_OP_METADATA");
                submitDataStoreOp(containerRuntime, "3", stagedOpContents, "LOCAL_OP_METADATA");
                containerRuntime.flush();
                node_assert_1.strict.equal(submittedOps.length, 1, "No more ops expected while staged");
                controls.discardChanges();
                node_assert_1.strict.deepEqual(channelCollectionStub.rollbackDataStoreOp.getCalls().map((call) => call.args), [
                    // LIFO order for rolling back
                    [{ address: "3", contents: stagedOpContents }, "LOCAL_OP_METADATA"],
                    [{ address: "2", contents: stagedOpContents }, "LOCAL_OP_METADATA"],
                    // Doesn't rollback the op from before staging mode
                ], "Unexpected args for rollback");
                (0, node_assert_1.strict)(channelCollectionStub.notifyStagingMode.getCall(1)?.calledWithExactly(false), "Expected notifyStagingMode to be called with false on the second call");
                node_assert_1.strict.equal(submittedOps.length, 1, // From before staging mode
                "Staged op should NOT be submitted after discardChanges");
            });
            it("discardChanges resets isDirty state", () => {
                stubChannelCollection(containerRuntime);
                const controls = containerRuntime.enterStagingMode();
                submitDataStoreOp(containerRuntime, "1", genTestDataStoreMessage("staged-op"), "LOCAL_OP_METADATA");
                submitDataStoreOp(containerRuntime, "2", genTestDataStoreMessage("staged-op"), "LOCAL_OP_METADATA");
                node_assert_1.strict.equal(containerRuntime.isDirty, true, "Runtime should be dirty (from Outbox)");
                containerRuntime.flush();
                node_assert_1.strict.equal(containerRuntime.isDirty, true, "Runtime should be dirty (from PSM)");
                controls.discardChanges();
                node_assert_1.strict.equal(containerRuntime.isDirty, false, "Runtime should not be dirty anymore");
            });
            describe("stagingModeAutoFlushThreshold", () => {
                let runtimeWithThreshold;
                let mockContext;
                async function createRuntimeWithThreshold(threshold) {
                    mockContext = getMockContext();
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: mockContext,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            stagingModeAutoFlushThreshold: threshold,
                            // Disable grouped batching so each op is individually submitted to the wire,
                            // making it easier to verify op counts.
                            enableGroupedBatching: false,
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    return runtime;
                }
                afterEach(() => {
                    runtimeWithThreshold?.dispose();
                    runtimeWithThreshold = undefined;
                });
                it("ops accumulate under threshold during staging mode", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(10);
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    const controls = runtimeWithThreshold.enterStagingMode();
                    // Submit 5 ops across multiple turns — under the threshold of 10
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    await Promise.resolve();
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    await Promise.resolve();
                    submitDataStoreOp(runtimeWithThreshold, "3", genTestDataStoreMessage("op3"));
                    await Promise.resolve();
                    submitDataStoreOp(runtimeWithThreshold, "4", genTestDataStoreMessage("op4"));
                    await Promise.resolve();
                    submitDataStoreOp(runtimeWithThreshold, "5", genTestDataStoreMessage("op5"));
                    await Promise.resolve();
                    node_assert_1.strict.equal(submittedOps.length, 0, "No ops should be submitted while under threshold in staging mode");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 5, "All 5 ops should be in the outbox");
                    controls.commitChanges();
                    node_assert_1.strict.equal(submittedOps.length, 5, "All 5 ops should be submitted after commitChanges");
                });
                it("ops flush when threshold is reached", async () => {
                    const logger = new internal_5.MockLogger();
                    const threshold = 3;
                    mockContext = getMockContext({ logger });
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: mockContext,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            stagingModeAutoFlushThreshold: threshold,
                            enableGroupedBatching: false,
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    runtimeWithThreshold = runtime;
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    const controls = runtimeWithThreshold.enterStagingMode();
                    // Submit 3 ops — exactly at the threshold
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    node_assert_1.strict.equal(submittedOps.length, 0, "Under threshold, no flush yet");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 2, "2 ops in outbox");
                    submitDataStoreOp(runtimeWithThreshold, "3", genTestDataStoreMessage("op3"));
                    // The 3rd op reaches the threshold, so scheduleFlush falls through to normal scheduling
                    await Promise.resolve();
                    // Ops are not submitted to the wire during staging mode, but the outbox should be
                    // emptied (ops moved from outbox into PendingStateManager as a staged batch).
                    node_assert_1.strict.equal(submittedOps.length, 0, "Ops should not be submitted to wire while in staging mode");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 0, "Outbox should be empty after threshold flush");
                    // Exit staging mode and verify perf event includes auto-flush count
                    controls.commitChanges();
                    logger.assertMatchAny([
                        {
                            eventName: "ContainerRuntime:ExitStagingMode_commit_end",
                            category: "performance",
                        },
                    ]);
                });
                it("incoming ops break the batch regardless of threshold", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(Infinity);
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    runtimeWithThreshold.enterStagingMode();
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    node_assert_1.strict.equal(submittedOps.length, 0, "No ops submitted yet");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 2, "2 ops in outbox");
                    // Simulate an incoming op — bumps lastSequenceNumber and emits "op"
                    // The deltaManager "op" handler calls this.flush() directly,
                    // which moves pending ops from outbox into PSM (as staged batches).
                    const mockDeltaManager = mockContext.deltaManager;
                    ++mockDeltaManager.lastSequenceNumber;
                    mockDeltaManager.emit("op", {
                        clientId: mockClientId,
                        sequenceNumber: mockDeltaManager.lastSequenceNumber,
                        clientSequenceNumber: 1,
                        type: internal_2.MessageType.ClientJoin,
                        contents: "test content",
                    });
                    // Ops are not submitted to the wire during staging mode, but the incoming op
                    // should have flushed the outbox into PendingStateManager as a staged batch.
                    node_assert_1.strict.equal(submittedOps.length, 0, "Ops should not be submitted to wire while in staging mode");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 0, "Outbox should be empty after incoming op flush");
                });
                it("exit staging mode flushes remaining ops", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(Infinity);
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    const controls = runtimeWithThreshold.enterStagingMode();
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    submitDataStoreOp(runtimeWithThreshold, "3", genTestDataStoreMessage("op3"));
                    node_assert_1.strict.equal(submittedOps.length, 0, "No ops submitted while staging");
                    controls.commitChanges();
                    (0, node_assert_1.strict)(submittedOps.length > 0, "Ops should be submitted after commitChanges");
                });
                it("has no effect outside staging mode", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(Infinity);
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    // Submit ops without entering staging mode
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    // Normal turn-based flush should still happen
                    await Promise.resolve();
                    node_assert_1.strict.equal(submittedOps.length, 1, "Op should flush normally when not in staging mode");
                });
                it("default threshold suppresses turn-based flushing during staging mode", async () => {
                    // Create runtime WITHOUT explicit stagingModeAutoFlushThreshold — uses the default (1000)
                    mockContext = getMockContext();
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: mockContext,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {},
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    runtimeWithThreshold = runtime;
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    const controls = runtimeWithThreshold.enterStagingMode();
                    // Submit a few ops across turns — well under the default threshold
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    await Promise.resolve();
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    await Promise.resolve();
                    node_assert_1.strict.equal(submittedOps.length, 0, "Default threshold should suppress turn-based flushing during staging mode");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 2, "Both ops should still be in the outbox (unflushed)");
                    controls.commitChanges();
                });
                it("discardChanges flushes outbox before rollback", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(Infinity);
                    const channelCollectionStub = stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    const controls = runtimeWithThreshold.enterStagingMode();
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"), "META1");
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"), "META2");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 2, "2 ops in outbox before discard");
                    controls.discardChanges();
                    // Outbox should have been drained before rollback
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 0, "Outbox should be empty after discard");
                    // Staged ops should be rolled back (LIFO order)
                    node_assert_1.strict.equal(channelCollectionStub.rollbackDataStoreOp.callCount, 2, "Both ops should be rolled back");
                    // Nothing sent to wire
                    node_assert_1.strict.equal(submittedOps.length, 0, "No ops should be submitted to wire after discard");
                });
                it("config override takes precedence over runtime option", async () => {
                    const configThreshold = 5;
                    const runtimeOptionThreshold = 50;
                    mockContext = getMockContext({
                        settings: {
                            "Fluid.ContainerRuntime.StagingModeAutoFlushThreshold": configThreshold,
                        },
                    });
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: mockContext,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            stagingModeAutoFlushThreshold: runtimeOptionThreshold,
                            enableGroupedBatching: false,
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    runtimeWithThreshold = runtime;
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.stagingModeAutoFlushThreshold, configThreshold, "Config override threshold (5) should win over runtime option (50)");
                });
                it("runtime option takes precedence over default", async () => {
                    const runtimeOptionThreshold = 5;
                    runtimeWithThreshold = await createRuntimeWithThreshold(runtimeOptionThreshold);
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.stagingModeAutoFlushThreshold, runtimeOptionThreshold, "Runtime option (5) should win over default (1000)");
                });
                it("incoming non-runtime op breaks batch during staging mode", async () => {
                    runtimeWithThreshold = await createRuntimeWithThreshold(Infinity);
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    runtimeWithThreshold.enterStagingMode();
                    submitDataStoreOp(runtimeWithThreshold, "1", genTestDataStoreMessage("op1"));
                    submitDataStoreOp(runtimeWithThreshold, "2", genTestDataStoreMessage("op2"));
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 2, "2 ops in outbox");
                    // Simulate an incoming non-runtime op (e.g. ClientJoin signal)
                    // The deltaManager "op" handler calls this.flush() directly.
                    const mockDeltaManager = mockContext.deltaManager;
                    ++mockDeltaManager.lastSequenceNumber;
                    mockDeltaManager.emit("op", {
                        clientId: mockClientId,
                        sequenceNumber: mockDeltaManager.lastSequenceNumber,
                        clientSequenceNumber: 1,
                        type: internal_2.MessageType.ClientJoin,
                        contents: "test content",
                    });
                    node_assert_1.strict.equal(submittedOps.length, 0, "No ops sent to wire during staging mode");
                    node_assert_1.strict.equal(
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                    runtimeWithThreshold.outbox.mainBatchMessageCount, 0, "Outbox should be empty — non-runtime op should break the batch");
                });
                it("IdAllocation + reconnect while in staging mode does not hit coherency check", async () => {
                    // With JIT ID allocation, the ID alloc op is generated at flush time
                    // (via generateIdAllocationOp callback), so it naturally gets the correct refSeq.
                    // The "op" handler calls flush() directly, so the IdAllocation op still gets
                    // flushed before new ops with different refSeqs are submitted.
                    // Start disconnected so we can trigger IdAllocation on reconnect
                    mockContext = getMockContext({ connected: false });
                    const mockDeltaManager = mockContext.deltaManager;
                    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context: mockContext,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {
                            stagingModeAutoFlushThreshold: Infinity,
                            enableRuntimeIdCompressor: "on",
                        },
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    runtimeWithThreshold = runtime;
                    stubChannelCollection(runtimeWithThreshold);
                    submittedOps.length = 0;
                    runtimeWithThreshold.enterStagingMode();
                    // Generate 1st compressed ID while disconnected — queues an IdAllocation op
                    runtimeWithThreshold.idCompressor?.generateCompressedId();
                    // Reconnect — replayPendingStates submits IdAllocation op + calls scheduleFlush()
                    // scheduleFlush() returns early due to staging mode threshold suppression
                    changeConnectionState(runtimeWithThreshold, true, mockClientId);
                    // Simulate a remote op arriving (bumps refSeq)
                    // The "op" handler calls this.flush() directly, draining the IdAllocation op
                    ++mockDeltaManager.lastSequenceNumber;
                    mockDeltaManager.emit("op", {
                        clientId: mockClientId,
                        sequenceNumber: mockDeltaManager.lastSequenceNumber,
                        clientSequenceNumber: 1,
                        type: internal_2.MessageType.ClientJoin,
                        contents: "test content",
                    });
                    // Generate 2nd compressed ID — its IdAllocation op has a new refSeq
                    const id2 = runtimeWithThreshold.idCompressor?.generateCompressedId();
                    // This would throw outboxSequenceNumberCoherencyCheck if the first IdAllocation
                    // op wasn't flushed before the refSeq changed.
                    node_assert_1.strict.doesNotThrow(() => submitDataStoreOp(runtimeWithThreshold, "someDS", genTestDataStoreMessage({ id: id2 })), "Should not throw coherency check — IdAllocation op should have been flushed by the 'op' handler");
                });
            });
            it("enterStagingMode flushes any pending outbox contents as non-staged", async () => {
                const context = getMockContext();
                const { runtime: rawRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableGroupedBatching: false,
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const runtime = rawRuntime;
                stubChannelCollection(runtime);
                submittedOps.length = 0;
                // Submit ops before entering staging mode (not yet flushed — still in outbox)
                submitDataStoreOp(runtime, "1", genTestDataStoreMessage("pre1"));
                submitDataStoreOp(runtime, "2", genTestDataStoreMessage("pre2"));
                node_assert_1.strict.equal(submittedOps.length, 0, "Ops not yet flushed");
                // Enter staging mode — should flush pre-staging ops as non-staged
                const controls = runtime.enterStagingMode();
                node_assert_1.strict.equal(submittedOps.length, 2, "Pre-staging ops should be flushed on enterStagingMode");
                // Submit more ops while in staging mode
                submitDataStoreOp(runtime, "3", genTestDataStoreMessage("staged1"));
                runtime.flush();
                node_assert_1.strict.equal(submittedOps.length, 2, "Staged ops should NOT be submitted to wire");
                controls.commitChanges();
                node_assert_1.strict.equal(submittedOps.length, 3, "All ops should be submitted after commit");
                runtime.dispose();
            });
            it("reconnect breaks batch during staging mode", async () => {
                const context = getMockContext();
                const { runtime: rawRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableGroupedBatching: false,
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const runtime = rawRuntime;
                stubChannelCollection(runtime);
                submittedOps.length = 0;
                runtime.enterStagingMode();
                // Submit ops while connected (sitting in outbox under threshold)
                submitDataStoreOp(runtime, "1", genTestDataStoreMessage("op1"));
                submitDataStoreOp(runtime, "2", genTestDataStoreMessage("op2"));
                node_assert_1.strict.equal(
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                runtime.outbox.mainBatchMessageCount, 2, "2 ops in outbox before disconnect");
                // Disconnect
                changeConnectionState(runtime, false, "disconnectedClientId");
                // Reconnect — triggers flush and replayPendingStates
                changeConnectionState(runtime, true, mockClientId);
                node_assert_1.strict.equal(
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
                runtime.outbox.mainBatchMessageCount, 0, "Outbox should be drained after reconnect");
                runtime.dispose();
            });
            it("reconnect resubmits pre-staged batches", async () => {
                const context = getMockContext();
                const { runtime: rawRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                    context,
                    registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                    existing: false,
                    runtimeOptions: {
                        enableGroupedBatching: false,
                    },
                    provideEntryPoint: mockProvideEntryPoint,
                });
                const runtime = rawRuntime;
                stubChannelCollection(runtime);
                submittedOps.length = 0;
                // Submit ops BEFORE entering staging mode
                submitDataStoreOp(runtime, "pre1", genTestDataStoreMessage("pre-staging-op"));
                await Promise.resolve();
                node_assert_1.strict.equal(submittedOps.length, 1, "Pre-staging op should be submitted");
                // Enter staging mode and submit more ops
                const controls = runtime.enterStagingMode();
                submitDataStoreOp(runtime, "staged1", genTestDataStoreMessage("staged-op"), "STAGED_META");
                runtime.flush();
                node_assert_1.strict.equal(submittedOps.length, 1, "Staged op should not be submitted to wire");
                // Disconnect
                changeConnectionState(runtime, false, "disconnectedClientId");
                submittedOps.length = 0;
                // Reconnect — replayPendingStates resubmits all pending ops
                changeConnectionState(runtime, true, mockClientId);
                await Promise.resolve();
                // The pre-staging op should be resubmitted to the wire.
                // The staged op stays in PSM as staged (not sent to wire).
                (0, node_assert_1.strict)(submittedOps.length > 0, "Pre-staging op should be resubmitted after reconnect");
                const opsAfterReconnect = submittedOps.length;
                // Verify we can still commit the staged changes
                controls.commitChanges();
                (0, node_assert_1.strict)(submittedOps.length > opsAfterReconnect, "Staged op should be submitted after commitChanges");
                runtime.dispose();
            });
            describe("stagingModeChanged event", () => {
                it("emits with { inStagingMode: true } on enter, and inStagingMode is true when handler runs", () => {
                    const events = [];
                    const inStagingModeAtEventTime = [];
                    containerRuntime.on("stagingModeChanged", (e) => {
                        events.push(e);
                        inStagingModeAtEventTime.push(containerRuntime.inStagingMode);
                    });
                    containerRuntime.enterStagingMode();
                    node_assert_1.strict.equal(events.length, 1, "Expected exactly one event on enter");
                    node_assert_1.strict.deepEqual(events[0], { inStagingMode: true }, "Event payload should be { inStagingMode: true }");
                    node_assert_1.strict.equal(inStagingModeAtEventTime[0], true, "inStagingMode should be true when the enter event fires");
                });
                it("emits with { inStagingMode: false, commit: true } on commitChanges, and inStagingMode is false when handler runs", () => {
                    stubChannelCollection(containerRuntime);
                    const events = [];
                    const inStagingModeAtEventTime = [];
                    containerRuntime.on("stagingModeChanged", (e) => {
                        events.push(e);
                        inStagingModeAtEventTime.push(containerRuntime.inStagingMode);
                    });
                    const controls = containerRuntime.enterStagingMode();
                    controls.commitChanges();
                    node_assert_1.strict.equal(events.length, 2, "Expected enter + commit events");
                    node_assert_1.strict.deepEqual(events[1], { inStagingMode: false, commit: true }, "Commit event payload should be { inStagingMode: false, commit: true }");
                    node_assert_1.strict.equal(inStagingModeAtEventTime[1], false, "inStagingMode should be false when the commit event fires");
                });
                it("emits with { inStagingMode: false, commit: false } on discardChanges, and inStagingMode is false when handler runs", () => {
                    stubChannelCollection(containerRuntime);
                    const events = [];
                    const inStagingModeAtEventTime = [];
                    containerRuntime.on("stagingModeChanged", (e) => {
                        events.push(e);
                        inStagingModeAtEventTime.push(containerRuntime.inStagingMode);
                    });
                    const controls = containerRuntime.enterStagingMode();
                    controls.discardChanges();
                    node_assert_1.strict.equal(events.length, 2, "Expected enter + discard events");
                    node_assert_1.strict.deepEqual(events[1], { inStagingMode: false, commit: false }, "Discard event payload should be { inStagingMode: false, commit: false }");
                    node_assert_1.strict.equal(inStagingModeAtEventTime[1], false, "inStagingMode should be false when the discard event fires");
                });
                it("does not fire when orderSequentially uses staging mode internally (EnableRollback=true)", async () => {
                    // orderSequentially with EnableRollback calls enterStagingModeCore(silent=true)
                    // internally. The stagingModeChanged event should NOT be emitted.
                    const context = getMockContext({
                        settings: { "Fluid.ContainerRuntime.EnableRollback": true },
                    });
                    const { runtime: rawRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {},
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    const runtime = rawRuntime;
                    stubChannelCollection(runtime);
                    submittedOps.length = 0;
                    const events = [];
                    runtime.on("stagingModeChanged", (e) => events.push(e));
                    // internally enters staging mode then commits
                    runtime.orderSequentially(() => {
                        submitDataStoreOp(runtime, "1", genTestDataStoreMessage("op1"));
                    });
                    node_assert_1.strict.equal(events.length, 0, "stagingModeChanged should NOT fire for successful orderSequentially");
                    // internally enters staging mode then rolls back
                    node_assert_1.strict.throws(() => runtime.orderSequentially(() => {
                        throw new Error("test error");
                    }));
                    node_assert_1.strict.equal(events.length, 0, "stagingModeChanged should NOT fire when orderSequentially rolls back");
                    runtime.dispose();
                });
                it("still fires for explicit enterStagingMode() when EnableRollback=true", async () => {
                    // Sanity-check that the silent flag only suppresses orderSequentially's
                    // internal usage — direct calls to enterStagingMode() still emit normally.
                    const context = getMockContext({
                        settings: { "Fluid.ContainerRuntime.EnableRollback": true },
                    });
                    const { runtime: rawRuntime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
                        context,
                        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
                        existing: false,
                        runtimeOptions: {},
                        provideEntryPoint: mockProvideEntryPoint,
                    });
                    const runtime = rawRuntime;
                    stubChannelCollection(runtime);
                    submittedOps.length = 0;
                    const events = [];
                    runtime.on("stagingModeChanged", (e) => events.push(e));
                    const controls = runtime.enterStagingMode();
                    controls.commitChanges();
                    node_assert_1.strict.equal(events.length, 2, "Expected enter + commit events for explicit staging");
                    node_assert_1.strict.deepEqual(events[0], { inStagingMode: true }, "Enter event");
                    node_assert_1.strict.deepEqual(events[1], { inStagingMode: false, commit: true }, "Commit event");
                    runtime.dispose();
                });
                it("throws UsageError when calling commitChanges on stale controls after discard", () => {
                    stubChannelCollection(containerRuntime);
                    const controls = containerRuntime.enterStagingMode();
                    controls.discardChanges();
                    node_assert_1.strict.throws(() => controls.commitChanges(), (error) => error.errorType === internal_1.ContainerErrorTypes.usageError &&
                        error.message === "Not in staging mode", "Calling commitChanges on stale controls should throw UsageError");
                });
                it("throws UsageError when calling discardChanges on old controls after re-entering staging mode", () => {
                    stubChannelCollection(containerRuntime);
                    const oldControls = containerRuntime.enterStagingMode();
                    oldControls.discardChanges();
                    // Enter staging mode again — new controls
                    containerRuntime.enterStagingMode();
                    node_assert_1.strict.throws(() => oldControls.discardChanges(), (error) => error.errorType === internal_1.ContainerErrorTypes.usageError &&
                        error.message === "Not in staging mode", "Calling discardChanges on old controls should throw UsageError");
                });
                it("does not emit exit event when container is disposed while in staging mode", () => {
                    const events = [];
                    containerRuntime.on("stagingModeChanged", (e) => events.push(e));
                    containerRuntime.enterStagingMode();
                    node_assert_1.strict.equal(events.length, 1, "Expected enter event");
                    containerRuntime.dispose();
                    node_assert_1.strict.equal(events.length, 1, "No exit event should fire on dispose — only the original enter event");
                });
            });
        });
    });
});
//# sourceMappingURL=containerRuntime.spec.js.map