"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const node_assert_1 = require("node:assert");
const driver_definitions_1 = require("@fluidframework/driver-definitions");
const internal_1 = require("@fluidframework/runtime-definitions/internal");
const internal_2 = require("@fluidframework/runtime-utils/internal");
const internal_3 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../gc/index.js");
const summarizerNodeWithGc_js_1 = require("../summary/summarizerNode/summarizerNodeWithGc.js");
describe("SummarizerNodeWithGC Tests", () => {
    const summarizerNodeId = "testNode";
    const node1Id = "/gcNode1";
    const node2Id = "/gcNode2";
    const subNode1Id = "/gcNode1/subNode";
    const subNode2Id = "/gcNode2/subNode";
    let rootSummarizerNode;
    let summarizerNode;
    // The base GC details of the root summarizer node. The child base GC details from this is passed on to the child
    // summarizer node during its creation.
    let rootBaseGCDetails;
    // The GC data returned by the getGCData() API of the child summarizer node.
    let childInternalGCData;
    let mockLogger;
    const getRootBaseGCDetails = async () => rootBaseGCDetails;
    const getChildInternalGCData = async () => childInternalGCData;
    beforeEach(async () => {
        mockLogger = new internal_3.MockLogger();
        rootSummarizerNode = (0, summarizerNodeWithGc_js_1.createRootSummarizerNodeWithGC)(mockLogger, (() => undefined), 0, 0, undefined, undefined, getRootBaseGCDetails);
        rootSummarizerNode.startSummary(0, (0, internal_3.createChildLogger)(), 0);
        summarizerNode = rootSummarizerNode.createChild(summarizeInternal, summarizerNodeId, { type: internal_1.CreateSummarizerNodeSource.FromSummary }, undefined, getChildInternalGCData);
        // Initialize the values to be returned by the child node's getGCData.
        childInternalGCData = {
            gcNodes: {
                "/": [node1Id, node2Id],
                [node1Id]: [subNode1Id],
            },
        };
        // Initialize the values to be returned by the getBaseGCDetails() API of the root summarizer node.
        rootBaseGCDetails = {
            usedRoutes: [],
        };
    });
    async function summarizeInternal(fullTree, trackState) {
        const stats = (0, internal_2.mergeStats)();
        stats.treeNodeCount++;
        return {
            summary: {
                type: driver_definitions_1.SummaryType.Tree,
                tree: {},
            },
            stats,
            id: summarizerNodeId,
        };
    }
    /**
     * Given the GC data of a child, build the GC data of the root (parent) node.
     */
    function buildRootGCData(childGCData, childId) {
        const builder = new internal_2.GCDataBuilder();
        builder.prefixAndAddNodes(childId, childGCData.gcNodes);
        return builder.getGCData();
    }
    describe("getGCData API", () => {
        it("fails when function to get GC data is not provided", async () => {
            // Root summarizer node does not have the function to get GC data. Trying to get GC data from it should
            // fail.
            let failed = false;
            try {
                await rootSummarizerNode.getGCData();
            }
            catch {
                failed = true;
            }
            (0, node_assert_1.strict)(failed, "Getting GC data should have failed");
        });
        it("can return GC data when data has changed since last summary", async () => {
            // Invalidate the summarizer node to force it to generate GC data and not use cached value.
            summarizerNode.invalidate(10);
            const gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, childInternalGCData, "GC data should be generated by calling getChildInternalGCData");
        });
        it("can return base GC data when nothing has changed since last summary", async () => {
            // The base GC data of the child summarizer node.
            const childBaseGCData = {
                gcNodes: {
                    "/": [node1Id],
                    [node1Id]: ["/"],
                    [node2Id]: [subNode1Id, subNode2Id],
                },
            };
            // Set the root base GC details to include the child node's base GC data.
            rootBaseGCDetails = {
                usedRoutes: [""],
                gcData: buildRootGCData(childBaseGCData, summarizerNodeId),
            };
            // We did not invalidate the summarizer node, so it will get the base GC data because nothing changed
            // since last summary.
            const gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, childBaseGCData, "Base GC data should have been returned");
        });
        it("can return GC data when base GC data is not available", async () => {
            // The base GC data of the child summarizer node is undefined by default. So, the node will generate GC
            // data even though nothing changed since last summary.
            const gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, childInternalGCData, "GC data should be generated by calling getChildInternalGCData");
        });
        it("can return cached GC data", async () => {
            // The base GC data of the child summarizer node is undefined by default. So, the node will generate GC
            // data even though nothing changed since last summary.
            let gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, childInternalGCData, "GC data should be generated by calling getChildInternalGCData");
            // Make a clone of the GC data returned above because we are about to change it.
            const cachedGCData = (0, index_js_1.cloneGCData)(gcData);
            // Add a new node to the GC data returned by getChildInternalGCData to make it different from cachedGCData above.
            // This will validate that the data returned by getGCData is not childInternalGCData.
            childInternalGCData.gcNodes[subNode1Id] = ["/", subNode2Id];
            // Since nothing changed since last summary, summarizer node should return the data from the previous run.
            gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, cachedGCData, "GC data from previous run should be returned");
        });
        it("can generate GC data when nothing changed but fullGC flag is true", async () => {
            let gcData = await summarizerNode.getGCData();
            node_assert_1.strict.deepStrictEqual(gcData, childInternalGCData, "GC data should be generated by calling getChildInternalGCData");
            // Add a new node to the GC data returned by getChildInternalGCData to make it different from before.
            // This will validate that the data returned by getGCData is the new childInternalGCData.
            childInternalGCData.gcNodes[subNode1Id] = ["/", subNode2Id];
            // Call getGCData() with fullGC = true. Even though nothing changed since last summary, this will force the
            // summarizer node to generate GC data by calling getChildInternalGCData.
            gcData = await summarizerNode.getGCData(true /* fullGC */);
            node_assert_1.strict.deepStrictEqual(gcData, childInternalGCData, "GC data should be generated by calling getChildInternalGCData");
        });
    });
    describe("Re-summarization due to GC state changes", () => {
        /**
         * Re-summarization is triggered due to GC state change if summarizer node's "hasUsedStateChanged"
         * returns true.
         */
        it("should not trigger re-summarization if used routes don't change", async () => {
            const usedRoutes = ["/route"];
            const baseGCDetails = {
                gcData: {
                    gcNodes: {},
                },
                usedRoutes,
            };
            summarizerNode.baseGCDetailsP = Promise.resolve(baseGCDetails);
            await summarizerNode.loadBaseGCDetails();
            summarizerNode.updateUsedRoutes(usedRoutes);
            node_assert_1.strict.strictEqual(summarizerNode.hasUsedStateChanged(), false, "Re-summarization should not be triggered");
        });
        it("should trigger re-summarization if used routes changes from base snapshot", async () => {
            const usedRoutes = ["/route"];
            const baseGCDetails = {
                gcData: {
                    gcNodes: {},
                },
                usedRoutes,
            };
            summarizerNode.baseGCDetailsP = Promise.resolve(baseGCDetails);
            await summarizerNode.loadBaseGCDetails();
            summarizerNode.updateUsedRoutes([...usedRoutes, "/newRoute"]);
            node_assert_1.strict.strictEqual(summarizerNode.hasUsedStateChanged(), true, "Re-summarization should be triggered");
        });
        it("should trigger re-summarization if base snapshot used routes is empty", async () => {
            const baseGCDetails = {
                gcData: {
                    gcNodes: {},
                },
                usedRoutes: undefined,
            };
            summarizerNode.baseGCDetailsP = Promise.resolve(baseGCDetails);
            await summarizerNode.loadBaseGCDetails();
            summarizerNode.updateUsedRoutes(["/newRoute"]);
            node_assert_1.strict.strictEqual(summarizerNode.hasUsedStateChanged(), true, "Re-summarization should be triggered");
        });
        it("should not trigger re-summarization if base snapshot used routes is empty and GC is disabled", async () => {
            const summarizerNodeGCDisabled = rootSummarizerNode.createChild(summarizeInternal, "nodeGCDisabled", { type: internal_1.CreateSummarizerNodeSource.FromSummary }, { gcDisabled: true }, getChildInternalGCData);
            const baseGCDetails = {
                gcData: {
                    gcNodes: {},
                },
                usedRoutes: undefined,
            };
            summarizerNodeGCDisabled.baseGCDetailsP = Promise.resolve(baseGCDetails);
            await summarizerNodeGCDisabled.loadBaseGCDetails();
            summarizerNodeGCDisabled.updateUsedRoutes(["/newRoute"]);
            node_assert_1.strict.strictEqual(summarizerNodeGCDisabled.hasUsedStateChanged(), false, "Re-summarization should not be triggered");
        });
    });
    describe("Validate Summary", () => {
        const nodeIds = {
            rootId: "", // emptry string to emulate the current behaviour of the summarizerNode for root.
            midId: "midId",
            leafId: "leafId",
        };
        let rootNode;
        let midNode;
        let leafNode;
        const logger = (0, internal_3.createChildLogger)();
        const getSummarizeInternalFn = (id) => async (fullTree) => {
            return {
                id,
                pathPartsForChildren: undefined, // extra path parts between nodes
                stats: (0, internal_2.mergeStats)(),
                summary: { type: driver_definitions_1.SummaryType.Tree, tree: {} },
            };
        };
        function createRoot({ changeSeq = 1, refSeq, ...config } = {}) {
            rootNode = (0, summarizerNodeWithGc_js_1.createRootSummarizerNodeWithGC)(logger, getSummarizeInternalFn(nodeIds.rootId), changeSeq, refSeq, config);
        }
        function createMid(createParam) {
            midNode = rootNode.createChild(getSummarizeInternalFn(nodeIds.midId), nodeIds.midId, createParam);
        }
        function createLeaf(createParam) {
            leafNode = midNode?.createChild(getSummarizeInternalFn(nodeIds.leafId), nodeIds.leafId, createParam);
        }
        it("summary validation should fail if GC not run on root node", () => {
            createRoot();
            rootNode.startSummary(11, logger, 0);
            // Validate summary fails by calling validateSummary.
            const expectedResult = {
                success: false,
                reason: "NodeDidNotRunGC",
                id: {
                    tag: internal_3.TelemetryDataTag.CodeArtifact,
                    value: "",
                },
                retryAfterSeconds: 1,
            };
            const result = rootNode.validateSummary();
            node_assert_1.strict.deepStrictEqual(result, expectedResult, "validate summary should have failed at the root node");
        });
        it("summary validation should fail if GC not run on child node", async () => {
            createRoot();
            createMid({ type: internal_1.CreateSummarizerNodeSource.Local });
            createLeaf({ type: internal_1.CreateSummarizerNodeSource.Local });
            rootNode.startSummary(11, logger, 0);
            // Call updateUsedRoutes (indicating GC ran) and summarize on the root and leaf nodes but not on the
            // mid node. Calling summarize is important because otherwise we will see similar failures because of
            // not running GC.
            rootNode.updateUsedRoutes([""]);
            leafNode?.updateUsedRoutes([""]);
            await rootNode.summarize(false);
            await leafNode?.summarize(false);
            const midNodeId = `/${nodeIds.midId}`;
            // Validate summary fails by calling validateSummary.
            const expectedResult = {
                success: false,
                reason: "NodeDidNotRunGC",
                id: {
                    tag: internal_3.TelemetryDataTag.CodeArtifact,
                    value: midNodeId,
                },
                retryAfterSeconds: 1,
            };
            const result = rootNode.validateSummary();
            node_assert_1.strict.deepStrictEqual(result, expectedResult, "validate summary should have failed at the mid node");
        });
        it("summary validation should fail if GC not run on leaf node", async () => {
            createRoot();
            createMid({ type: internal_1.CreateSummarizerNodeSource.Local });
            createLeaf({ type: internal_1.CreateSummarizerNodeSource.Local });
            rootNode.startSummary(11, logger, 0);
            // Call updateUsedRoutes (indicating GC ran) and summarize on the root and leaf nodes but not on the
            // mid node. Calling summarize is important because otherwise we will see similar failures because of
            // not running GC.
            rootNode.updateUsedRoutes([""]);
            midNode?.updateUsedRoutes([""]);
            await rootNode.summarize(false);
            await midNode?.summarize(false);
            const leafNodeId = `/${nodeIds.midId}/${nodeIds.leafId}`;
            // Validate summary fails by calling validateSummary.
            const expectedResult = {
                success: false,
                reason: "NodeDidNotRunGC",
                id: {
                    tag: internal_3.TelemetryDataTag.CodeArtifact,
                    value: leafNodeId,
                },
                retryAfterSeconds: 1,
            };
            const result = rootNode.validateSummary();
            node_assert_1.strict.deepStrictEqual(result, expectedResult, "validate summary should have failed at the leaf node");
        });
        let summaryRefSeq = 123;
        it("Should add GC pending summary node created after parent node was summarized with non-empty used routes", async () => {
            createRoot();
            createMid({ type: internal_1.CreateSummarizerNodeSource.Local });
            const latestSummaryRefSeqNum = summaryRefSeq;
            rootNode.startSummary(summaryRefSeq++, logger, 0);
            rootNode.updateUsedRoutes([""]);
            midNode?.updateUsedRoutes([""]);
            await rootNode.summarize(false);
            await midNode?.summarize(false);
            rootNode.completeSummary("test-handle1");
            let result = await rootNode.refreshLatestSummary("test-handle1", summaryRefSeq);
            (0, node_assert_1.strict)(result.isSummaryTracked, "should be tracked");
            (0, node_assert_1.strict)(result.isSummaryNewer === true, "should be newer");
            rootNode.startSummary(summaryRefSeq++, logger, latestSummaryRefSeqNum);
            rootNode.updateUsedRoutes([
                `/`,
                `/${nodeIds.midId}`,
                `/${nodeIds.midId}/${nodeIds.leafId}`,
            ]);
            midNode?.updateUsedRoutes([`/`, `/${nodeIds.leafId}`]);
            await rootNode.summarize(false);
            await midNode?.summarize(false);
            rootNode.completeSummary("test-handle2");
            // Create a new child node for which we will need to create a pending summary for.
            createLeaf({ type: internal_1.CreateSummarizerNodeSource.Local });
            result = await rootNode.refreshLatestSummary("test-handle2", summaryRefSeq);
            (0, node_assert_1.strict)(result.isSummaryTracked, "should be tracked");
            (0, node_assert_1.strict)(result.isSummaryNewer === true, "should be newer");
            const leafNodePath = `${nodeIds.rootId}/${internal_1.channelsTreeName}/${nodeIds.midId}/${internal_1.channelsTreeName}/${nodeIds.leafId}`;
            const leafNodeLatestSummaryHandle = leafNode.summaryHandleId;
            node_assert_1.strict.strictEqual(leafNodeLatestSummaryHandle, leafNodePath, "The child node's latest summary path is incorrect");
        });
        it("Should add GC pending summary node created after parent node was summarized with empty used routes", async () => {
            createRoot();
            createMid({ type: internal_1.CreateSummarizerNodeSource.Local });
            const latestSummaryRefSeqNum = summaryRefSeq;
            rootNode.startSummary(summaryRefSeq++, logger, 0);
            rootNode.updateUsedRoutes([""]);
            midNode?.updateUsedRoutes([""]);
            await rootNode.summarize(false);
            await midNode?.summarize(false);
            rootNode.completeSummary("test-handle1");
            let result = await rootNode.refreshLatestSummary("test-handle1", summaryRefSeq);
            (0, node_assert_1.strict)(result.isSummaryTracked, "should be tracked");
            (0, node_assert_1.strict)(result.isSummaryNewer === true, "should be newer");
            rootNode.startSummary(summaryRefSeq++, logger, latestSummaryRefSeqNum);
            rootNode.updateUsedRoutes([""]);
            midNode?.updateUsedRoutes([""]);
            await rootNode.summarize(false);
            await midNode?.summarize(false);
            rootNode.completeSummary("test-handle2");
            // Create a new child node for which we will need to create a pending summary for.
            createLeaf({ type: internal_1.CreateSummarizerNodeSource.Local });
            result = await rootNode.refreshLatestSummary("test-handle2", summaryRefSeq);
            (0, node_assert_1.strict)(result.isSummaryTracked, "should be tracked");
            (0, node_assert_1.strict)(result.isSummaryNewer === true, "should be newer");
            const leafNodePath = `${nodeIds.rootId}/${internal_1.channelsTreeName}/${nodeIds.midId}/${internal_1.channelsTreeName}/${nodeIds.leafId}`;
            const leafNodeLatestSummaryHandle = leafNode.summaryHandleId;
            node_assert_1.strict.strictEqual(leafNodeLatestSummaryHandle, leafNodePath, "The child node's latest summary path is incorrect");
        });
    });
});
//# sourceMappingURL=summarizerNodeWithGc.spec.js.map