/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { buildSchema, type GraphQLSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { applyCommand } from "../apply-command.js"; import { renderQuery } from "../query-builder.js"; import { createSession } from "../session.js"; import { primeSchemaCache } from "../walker.js"; const SCHEMA: GraphQLSchema = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { Case(first: Int, after: String, where: Case_Filter, orderBy: Case_OrderBy): CaseConnection! } input Case_Filter { Status: PicklistOperators, Subject: StringOperators, Amount: DoubleOperators } input Case_OrderBy { CreatedDate: OrderByClause } input PicklistOperators { eq: String, ne: String, in: [String!] } input StringOperators { eq: String, like: String } input DoubleOperators { eq: Float, gt: Float } input OrderByClause { order: Order! } enum Order { ASC DESC } type CaseConnection { edges: [CaseEdge!]!, pageInfo: PageInfo! } type CaseEdge { node: Case! } type PageInfo { hasNextPage: Boolean!, endCursor: String } type Case { Id: ID!, Subject: StringValue, Status: StringValue } type StringValue { value: String } `); const ORG = "test-apply"; const ORG_URL = `https://${ORG}.my.salesforce.com`; primeSchemaCache(ORG_URL, SCHEMA); describe("lib/apply-command — routing", () => { it("throws on an unknown verb", () => { const session = createSession(ORG, "query", ORG_URL); expect(() => applyCommand(session, SCHEMA, "cd uiapi")).toThrow(/unknown command 'cd'/); }); it("throws on an empty command", () => { const session = createSession(ORG, "query", ORG_URL); expect(() => applyCommand(session, SCHEMA, " ")).toThrow(/empty command/); }); }); describe("lib/apply-command — select", () => { it("selects a dotted leaf path with { value } wrapper", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "select uiapi/query/Case/edges/node/Subject/value"); const q = renderQuery(session); expect(q).toMatch(/Subject\s*\{\s*value\s*\}/); }); it("selects multiple space-separated leaves in one command", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand( session, SCHEMA, "select uiapi/query/Case/edges/node/Id uiapi/query/Case/edges/node/Subject/value", ); const q = renderQuery(session); expect(q).toMatch(/\bId\b/); expect(q).toMatch(/Subject\s*\{\s*value\s*\}/); }); it("applies a :alias to a selected leaf", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "select uiapi/query/Case/edges/node/Subject/value:subj"); const q = renderQuery(session); // Alias is applied to the leaf field (value), not to the parent (Subject) expect(q).toMatch(/subj:\s*value/); }); it("throws when no selectable item is supplied", () => { const session = createSession(ORG, "query", ORG_URL); expect(() => applyCommand(session, SCHEMA, "select")).toThrow(); }); }); describe("lib/apply-command — var", () => { it("declares a variable with type inferred from the path and binds it", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "var $status uiapi/query/Case/@args/where/Status/eq"); const q = renderQuery(session); // declared in the operation signature... expect(q).toMatch(/\$status:\s*String/); // ...and bound into the where arg. expect(q).toMatch(/Status:\s*\{\s*eq:\s*\$status\s*\}/); }); it("throws when no variable name is given", () => { const session = createSession(ORG, "query", ORG_URL); expect(() => applyCommand(session, SCHEMA, "var")).toThrow(); }); // W-22697670: re-declaring a $var with a conflicting type is rejected (first-wins // keeps the original type; the define/var path surfaces the collision instead of // silently misreporting the new type and producing an invalid query). it("throws when a $var is re-declared with a conflicting type", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "var $x uiapi/query/Case/@args/where/Status/eq"); // String expect(() => applyCommand(session, SCHEMA, "var $x uiapi/query/Case/@args/where/Amount/gt"), ).toThrow(/already declared as String; cannot redefine it as Float/); // First-wins: $x stays String and is not bound at the Float position. const q = renderQuery(session); expect(q).toMatch(/\$x:\s*String/); expect(q).not.toMatch(/\$x:\s*Float/); }); }); describe("lib/apply-command — set", () => { it("sets a scalar arg via key=value with an absolute field path", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case first=5"); expect(renderQuery(session)).toMatch(/first:\s*5/); }); it("expands where.Field=Value into a filter object", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case where.Status=New"); expect(renderQuery(session)).toMatch(/Status:\s*\{\s*eq:\s*"?New"?\s*\}/); }); it("auto-detects the like operator on a % wildcard value", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case where.Subject=Acme%"); expect(renderQuery(session)).toMatch(/Subject:\s*\{\s*like:/); }); it("expands orderBy=Field:DESC into an orderBy clause", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case orderBy=CreatedDate:DESC"); expect(renderQuery(session)).toMatch(/CreatedDate:\s*\{\s*order:\s*DESC\s*\}/); }); it("binds where.Field=$var as a deferred variable", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case where.Status=$status"); const q = renderQuery(session); expect(q).toMatch(/\$status:/); expect(q).toMatch(/Status:\s*\{\s*eq:\s*\$status\s*\}/); }); it("throws on a malformed set with no key=value", () => { const session = createSession(ORG, "query", ORG_URL); expect(() => applyCommand(session, SCHEMA, "set")).toThrow(); }); }); describe("lib/apply-command — set additional branches", () => { it("accepts the legacy `set ` pair form", () => { const session = createSession(ORG, "query", ORG_URL); // The pair-form branch passes the path verbatim to assignViaPath (no // `query/` auto-prefix), so the path must carry the operation-root // `query/` segment that resolveAliasSegments keys off — same form the // CLI dispatcher's pair branch requires. applyCommand(session, SCHEMA, "set query/uiapi/query/Case/@args/first 7"); expect(renderQuery(session)).toMatch(/first:\s*7/); }); it("cursor shorthand declares $after and selects pageInfo", () => { const session = createSession(ORG, "query", ORG_URL); // `cursor` must be mixed with a key=value so the parser routes through the // hasKeyValue branch (which captures the field prefix). A bare // `set cursor` falls into the pair-loop and errors in both this // port and the canonical CLI dispatcher, so the realistic form is mixed. applyCommand(session, SCHEMA, "set uiapi/query/Case first=5 cursor"); const q = renderQuery(session); expect(q).toMatch(/\$after:\s*String/); expect(q).toMatch(/after:\s*\$after/); expect(q).toMatch(/hasNextPage/); expect(q).toMatch(/endCursor/); }); it("applies multiple specs in one set command", () => { const session = createSession(ORG, "query", ORG_URL); applyCommand(session, SCHEMA, "set uiapi/query/Case first=5 where.Status=New"); const q = renderQuery(session); expect(q).toMatch(/first:\s*5/); expect(q).toMatch(/Status:\s*\{\s*eq:\s*"?New"?\s*\}/); }); });