/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { describe, expect, it } from "vitest"; import { makeSession } from "../../__tests__/helpers/schema.js"; import { renderQuery } from "../query-builder.js"; import type { VariableTypeCollision } from "../session.js"; import { addVariable, appendListElement, buildRuntimeVariables, cloneSession, createSiblingFieldInstance, deepGetArg, deepRemoveArg, deepSetArg, deepSetVariableValue, getArgsFieldPath, getInputSubPath, getNavigationContext, isInArgsContext, loadSession, parseVariablePath, queryNavToSchemaPath, removeListElement, saveSession, selectLeaf, setAliasOnPath, setArg, } from "../session.js"; describe("session", () => { describe("navigation context helpers", () => { it("getNavigationContext returns correct contexts", () => { expect(getNavigationContext([])).toBe("root"); expect(getNavigationContext(["query"])).toBe("query"); expect(getNavigationContext(["query", "uiapi"])).toBe("query"); expect(getNavigationContext(["variables"])).toBe("variables"); expect(getNavigationContext(["variables", "$filter"])).toBe("variables"); }); it("isInArgsContext detects @args segment", () => { expect(isInArgsContext(["query", "accounts", "@args"])).toBe(true); expect(isInArgsContext(["query", "accounts", "@args", "where"])).toBe(true); expect(isInArgsContext(["query", "accounts"])).toBe(false); expect(isInArgsContext([])).toBe(false); }); it("getArgsFieldPath extracts field path before @args", () => { expect(getArgsFieldPath(["query", "accounts", "@args", "where"])).toEqual(["accounts"]); expect(getArgsFieldPath(["query", "viewer", "@args"])).toEqual(["viewer"]); expect(getArgsFieldPath([])).toEqual([]); }); it("getInputSubPath extracts path after @args", () => { expect(getInputSubPath(["query", "accounts", "@args", "where", "name"])).toEqual([ "where", "name", ]); expect(getInputSubPath(["query", "accounts", "@args"])).toEqual([]); expect(getInputSubPath([])).toEqual([]); }); it("queryNavToSchemaPath strips query prefix and stops at @args", () => { expect(queryNavToSchemaPath(["query", "accounts"])).toEqual(["accounts"]); expect(queryNavToSchemaPath(["query", "accounts", "@args", "where"])).toEqual(["accounts"]); expect(queryNavToSchemaPath([])).toEqual([]); expect(queryNavToSchemaPath(["variables"])).toEqual([]); }); it("parseVariablePath extracts variable name and sub-path", () => { const result = parseVariablePath(["variables", "$filter", "name"]); expect(result).not.toBeNull(); expect(result!.varName).toBe("filter"); expect(result!.inputSubPath).toEqual(["name"]); expect(parseVariablePath(["query"])).toBeNull(); expect(parseVariablePath(["variables"])).toBeNull(); }); }); describe("deep set/get helpers", () => { it("deepSetArg creates nested JSON structure", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "10"); selectLeaf(session, ["accounts", "edges", "node", "name"]); deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"'); const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!; expect(raw.kind).toBe("field"); if (raw.kind !== "field") throw new Error("unreachable"); const parsed = JSON.parse(raw.args["where"]); expect(parsed).toEqual({ name: { like: "Acme%" } }); }); it("deepSetArg merges with existing values", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"'); deepSetArg(session, ["accounts"], "where", ["minRevenue"], "1000"); const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!; expect(raw.kind).toBe("field"); if (raw.kind !== "field") throw new Error("unreachable"); const parsed = JSON.parse(raw.args["where"]); expect(parsed).toEqual({ name: { like: "Acme%" }, minRevenue: 1000 }); }); it("deepSetArg with empty inputPath sets top-level arg directly", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); deepSetArg(session, ["accounts"], "first", [], "10"); const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!; expect(raw.kind).toBe("field"); if (raw.kind !== "field") throw new Error("unreachable"); expect(raw.args["first"]).toBe("10"); }); it("deepGetArg retrieves nested values", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"'); const value = deepGetArg(session, ["accounts"], "where", ["name", "like"]); expect(value).toBe("Acme%"); const whole = deepGetArg(session, ["accounts"], "where", []); expect(typeof whole).toBe("string"); }); it("deepRemoveArg removes nested values", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"'); deepSetArg(session, ["accounts"], "where", ["minRevenue"], "100"); const removed = deepRemoveArg(session, ["accounts"], "where", ["name", "like"]); expect(removed).toBe(true); const remaining = deepGetArg(session, ["accounts"], "where", ["minRevenue"]); expect(remaining).toBe(100); }); it("deepSetVariableValue builds nested variable runtime values", () => { const session = makeSession(); addVariable(session, "filter", "AccountFilter"); deepSetVariableValue(session, "filter", ["name", "like"], '"Test%"'); const variable = session.variables.find((v) => v.name === "filter")!; expect(variable.runtimeValue).toBeDefined(); const parsed = JSON.parse(variable.runtimeValue!); expect(parsed).toEqual({ name: { like: "Test%" } }); }); }); describe("list element management", () => { it("appendListElement creates array elements", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); const idx0 = appendListElement(session, ["accounts"], "orderBy", []); expect(idx0).toBe(0); const idx1 = appendListElement(session, ["accounts"], "orderBy", []); expect(idx1).toBe(1); const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!; expect(raw.kind).toBe("field"); if (raw.kind !== "field") throw new Error("unreachable"); const parsed = JSON.parse(raw.args["orderBy"]); expect(Array.isArray(parsed)).toBe(true); expect(parsed).toHaveLength(2); }); it("removeListElement splices array", () => { const session = makeSession(); selectLeaf(session, ["accounts", "edges", "node", "name"]); appendListElement(session, ["accounts"], "orderBy", []); appendListElement(session, ["accounts"], "orderBy", []); const removed = removeListElement(session, ["accounts"], "orderBy", [], 0); expect(removed).toBe(true); const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!; expect(raw.kind).toBe("field"); if (raw.kind !== "field") throw new Error("unreachable"); const parsed = JSON.parse(raw.args["orderBy"]); expect(parsed).toHaveLength(1); }); }); describe("session migration", () => { it("loadSession auto-migrates old navigation paths", () => { const session = makeSession(); session.navigationPath = ["accounts"]; // old-style path without "query" prefix saveSession(session); const loaded = loadSession(session.id); expect(loaded.navigationPath).toEqual(["query", "accounts"]); }); it("loadSession does not double-prefix already-migrated paths", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; saveSession(session); const loaded = loadSession(session.id); expect(loaded.navigationPath).toEqual(["query", "accounts"]); }); }); describe("aliasing & cloning", () => { it("supports multiple instances of the same field with different variables", () => { const session = makeSession(); addVariable(session, "$limit", "Int"); addVariable(session, "$otherLimit", "Int"); addVariable(session, "$accountFilter", "AccountFilter"); addVariable(session, "$otherFilter", "AccountFilter"); session.navigationPath = ["query", "accounts"]; setAliasOnPath(session, ["accounts"], "highRevenueAccounts"); setArg(session, ["accounts"], "first", "$limit"); setArg(session, ["accounts"], "where", "$accountFilter"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); session.navigationPath = ["query", "accounts"]; createSiblingFieldInstance(session, ["accounts"], "regionalAccounts"); setArg(session, ["accounts"], "first", "$otherLimit"); setArg(session, ["accounts"], "where", "$otherFilter"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "id"]); // Assertion lives in query-builder.spec.ts; here we only verify session state changes. const aliased = session.nodes.filter((n) => n.kind === "field" && n.fieldName === "accounts"); expect(aliased.length).toBeGreaterThanOrEqual(2); }); it("cloneSession produces a deep copy with a new id", () => { const session = makeSession(); addVariable(session, "limit", "Int", "10"); session.navigationPath = ["query", "accounts"]; setAliasOnPath(session, ["accounts"], "myAccounts"); setArg(session, ["accounts"], "first", "5"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const cloned = cloneSession(session, "my-clone"); expect(cloned.id).not.toBe(session.id); expect(cloned.name).toBe("my-clone"); expect(cloned.variables).toHaveLength(session.variables.length); expect(cloned.nodes).toHaveLength(session.nodes.length); expect(renderQuery(cloned)).toBe(renderQuery(session)); addVariable(cloned, "extra", "String"); expect(cloned.variables.length).not.toBe(session.variables.length); }); }); describe("addVariable type-collision (W-22697670)", () => { it("returns undefined and records the entry for a new variable name", () => { const session = makeSession(); const collision = addVariable(session, "$limit", "Int"); expect(collision).toBeUndefined(); expect(session.variables).toHaveLength(1); expect(session.variables[0]).toMatchObject({ name: "limit", type: "Int" }); }); it("returns undefined when the same name is re-declared with the SAME type", () => { const session = makeSession(); const first = addVariable(session, "$status", "String"); const second = addVariable(session, "$status", "String"); expect(first).toBeUndefined(); expect(second).toBeUndefined(); expect(session.variables).toHaveLength(1); expect(session.variables[0]).toMatchObject({ name: "status", type: "String" }); }); it("keeps the first-declared type and reports a collision on a DIFFERENT type (first-wins)", () => { const session = makeSession(); const first = addVariable(session, "$amount", "String"); const second = addVariable(session, "$amount", "Float"); expect(first).toBeUndefined(); const collision: VariableTypeCollision | undefined = second; expect(collision).toEqual({ name: "amount", existingType: "String", ignoredType: "Float", }); // First-wins: the stored variable still has the FIRST type, not the conflicting one. expect(session.variables).toHaveLength(1); expect(session.variables[0]).toMatchObject({ name: "amount", type: "String" }); }); }); describe("runtime variable building", () => { it("runtime variables are built from runtime values and defaults", () => { const session = makeSession(); addVariable(session, "$limit", "Int", "10"); addVariable(session, "$filters", "AccountFilter"); addVariable(session, "$isActive", "Boolean"); session.variables.find((variable) => variable.name === "filters")!.runtimeValue = '{"minRevenue":1000}'; session.variables.find((variable) => variable.name === "isActive")!.runtimeValue = "true"; expect(buildRuntimeVariables(session)).toEqual({ limit: 10, filters: { minRevenue: 1000 }, isActive: true, }); }); }); });