/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { rmSync } from "node:fs"; import path from "node:path"; import { buildSchema, introspectionFromSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { atomicWriteJson } from "../../lib/fs-utils.js"; import { schemaCacheKeyForInstanceUrl, schemaDir, type SchemaMetadata, } from "../../lib/introspect.js"; import { type FieldMetadata, type ObjectInfoResult } from "../../lib/object-info.js"; import { type PrimeDeps } from "../../lib/prime-schema.js"; import { primeSchemaCache } from "../../lib/walker.js"; import { buildDiscover, type DiscoverDeps } from "../build-discover.js"; const ORG = "test-discover"; const ORG_URL = "https://test-discover.my.salesforce.com"; const SCHEMA = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { "Customer accounts" Account(first: Int, after: String): AccountConnection! Case(first: Int, after: String): CaseConnection! Contact(first: Int, after: String): ContactConnection! } type AccountConnection { edges: [AccountEdge!]! } type AccountEdge { node: Account! } type Account { Id: ID!, Name: StringValue } type CaseConnection { edges: [CaseEdge!]! } type CaseEdge { node: Case! } type Case { Id: ID!, Subject: StringValue } type ContactConnection { edges: [ContactEdge!]! } type ContactEdge { node: Contact! } type Contact { Id: ID!, LastName: StringValue } type StringValue { value: String } `); primeSchemaCache(ORG, SCHEMA); primeSchemaCache(ORG_URL, SCHEMA); function makePrimeDeps(): PrimeDeps { return { getOrgAuth: async () => ({ alias: ORG, username: "u", instanceUrl: ORG_URL, accessToken: "t", orgId: "00D", }), downloadSchema: async (auth) => { const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl); const filePath = path.join(schemaDir(), `${cacheKey}.json`); atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) }); const meta: SchemaMetadata = { cacheKey, instanceUrl: auth.instanceUrl, typeCount: 0, downloadedAt: new Date().toISOString(), filePath, }; return meta; }, }; } function makeField(overrides: Partial): FieldMetadata { return { apiName: "Field", label: "Field", dataType: "STRING", required: false, createable: false, updateable: false, calculated: false, custom: false, filterable: true, sortable: true, nameField: false, reference: false, relationshipName: null, compound: false, compoundFieldName: null, defaultedOnCreate: false, extraTypeInfo: null, inlineHelpText: null, precision: 0, scale: 0, referenceToInfos: [], controllerName: null, controllingFields: [], ...overrides, }; } const ACCOUNT_INFO: ObjectInfoResult = { apiName: "Account", label: "Account", labelPlural: "Accounts", createable: true, deletable: true, updateable: true, queryable: true, searchable: true, custom: false, keyPrefix: "001", nameFields: ["Name"], defaultRecordTypeId: null, fields: [ makeField({ apiName: "Id", label: "Account ID", dataType: "ID", filterable: true, sortable: true, createable: false, updateable: false, }), makeField({ apiName: "Name", label: "Account Name", dataType: "STRING", nameField: true, required: true, createable: true, updateable: true, }), makeField({ apiName: "Industry", label: "Industry", dataType: "PICKLIST", createable: true, updateable: true, }), makeField({ apiName: "OwnerId", label: "Owner ID", dataType: "REFERENCE", reference: true, relationshipName: "Owner", referenceToInfos: [ { apiName: "User", nameFields: ["Name"] }, { apiName: "Group", nameFields: ["Name"] }, ], createable: true, updateable: true, }), makeField({ apiName: "BillingAddress", label: "Billing Address", dataType: "ADDRESS", compound: true, filterable: false, sortable: false, }), makeField({ apiName: "AutoNumber__c", label: "Auto Number", dataType: "STRING", required: true, createable: true, defaultedOnCreate: true, }), ], childRelationships: [ { childObjectApiName: "Contact", fieldName: "AccountId", relationshipName: "Contacts" }, { childObjectApiName: "Case", fieldName: "AccountId", relationshipName: null }, ], recordTypeInfos: [], picklists: [ { apiName: "Industry", label: "Industry", required: false, values: [ { value: "Technology", label: "Technology" }, { value: "Finance", label: "Finance" }, ], }, ], fetchedAt: new Date().toISOString(), }; function makeDeps(): DiscoverDeps { return { primeDeps: makePrimeDeps(), getOrgAuth: async () => ({ alias: ORG, username: "u", instanceUrl: ORG_URL, accessToken: "t", orgId: "00D", }), getObjectInfo: async (_auth, _alias, name) => { if (name === "Account") return ACCOUNT_INFO; throw new Error(`No fixture for ${name}`); }, }; } describe("intent/build-discover", () => { describe("list_objects mode (FR-11.1)", () => { it("returns queryable SObjects sorted alphabetically", async () => { const out = await buildDiscover({ org: ORG, mode: "list_objects" }, makeDeps()); expect(out.mode).toBe("list_objects"); if (out.mode !== "list_objects") return; expect(out.objects.map((o) => o.name)).toEqual(["Account", "Case", "Contact"]); }); it("includes the schema description as the label when present", async () => { const out = await buildDiscover({ org: ORG, mode: "list_objects" }, makeDeps()); if (out.mode !== "list_objects") return; const account = out.objects.find((o) => o.name === "Account"); expect(account?.label).toBe("Customer accounts"); }); it("filters by search substring (case-insensitive)", async () => { const out = await buildDiscover( { org: ORG, mode: "list_objects", search: "cas" }, makeDeps(), ); if (out.mode !== "list_objects") return; expect(out.objects.map((o) => o.name)).toEqual(["Case"]); }); it("excludes non-Connection fields like search/aggregate helpers", async () => { const ALIAS = "test-discover-helpers"; const URL = "https://test-discover-helpers.my.salesforce.com"; const helperSchema = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { Account(first: Int): AccountConnection! search(term: String!): SearchResult! aggregate: AggregateResult! } type AccountConnection { edges: [AccountEdge!]! } type AccountEdge { node: Account! } type Account { Id: ID! } type SearchResult { hits: Int } type AggregateResult { total: Int } `); primeSchemaCache(ALIAS, helperSchema); primeSchemaCache(URL, helperSchema); const helperDeps: DiscoverDeps = { primeDeps: { getOrgAuth: async () => ({ alias: ALIAS, username: "u", instanceUrl: URL, accessToken: "t", orgId: "00D", }), downloadSchema: async (auth) => { const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl); const filePath = path.join(schemaDir(), `${cacheKey}.json`); atomicWriteJson(filePath, { data: introspectionFromSchema(helperSchema) }); return { cacheKey, instanceUrl: auth.instanceUrl, typeCount: 0, downloadedAt: new Date().toISOString(), filePath, }; }, }, }; const out = await buildDiscover({ org: ALIAS, mode: "list_objects" }, helperDeps); if (out.mode !== "list_objects") return; expect(out.objects.map((o) => o.name)).toEqual(["Account"]); }); it("surfaces the FR-13.3 priming note in warnings on cold prime", async () => { const ALIAS = "test-discover-cold"; const URL = "https://test-discover-cold.my.salesforce.com"; const coldSchema = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { Account(first: Int): AccountConnection! } type AccountConnection { edges: [AccountEdge!]! } type AccountEdge { node: Account! } type Account { Id: ID! } `); primeSchemaCache(URL, coldSchema); // downloadSchema below writes to the real ~/.graphiti/schemas/ dir. // The "cold prime" assertion requires that cache file to be ABSENT so // buildDiscover primes it and emits the FR-13.3 warning. A prior run // of this test leaves the file behind, so on the next run the cache is // already present, no priming happens, and the warning assertion fails. // Clear it before (restore the cold precondition) and after (no leak). const coldCachePath = path.join(schemaDir(), `${schemaCacheKeyForInstanceUrl(URL)}.json`); rmSync(coldCachePath, { force: true }); const coldDeps: DiscoverDeps = { primeDeps: { getOrgAuth: async () => ({ alias: ALIAS, username: "u", instanceUrl: URL, accessToken: "t", orgId: "00D", }), downloadSchema: async (auth) => { const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl); const filePath = path.join(schemaDir(), `${cacheKey}.json`); atomicWriteJson(filePath, { data: introspectionFromSchema(coldSchema) }); return { cacheKey, instanceUrl: auth.instanceUrl, typeCount: 0, downloadedAt: new Date().toISOString(), filePath, }; }, }, }; try { const out = await buildDiscover({ org: ALIAS, mode: "list_objects" }, coldDeps); if (out.mode !== "list_objects") return; expect(out.warnings).toBeDefined(); expect(out.warnings?.[0]).toMatch(/Primed schema cache for "test-discover-cold"/); } finally { rmSync(coldCachePath, { force: true }); } }); }); describe("describe_object mode (FR-11.2)", () => { it("returns object metadata with picklists, child relationships, parent refs", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_object", object: "Account" }, makeDeps(), ); expect(out.mode).toBe("describe_object"); if (out.mode !== "describe_object") return; expect(out.object.name).toBe("Account"); const fieldNames = out.object.fields.map((f) => f.name); expect(fieldNames).toContain("Id"); expect(fieldNames).toContain("Name"); const industry = out.object.fields.find((f) => f.name === "Industry"); expect(industry?.picklistValues).toEqual(["Technology", "Finance"]); expect(out.object.childRelationships).toEqual([ { relationshipName: "Contacts", childObject: "Contact" }, ]); expect(out.object.parentReferences).toEqual([ { field: "OwnerId", targetObjects: ["User", "Group"] }, ]); }); it("filterableFields excludes compound fields", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_object", object: "Account" }, makeDeps(), ); if (out.mode !== "describe_object") return; expect(out.object.filterableFields).not.toContain("BillingAddress"); expect(out.object.filterableFields).toContain("Name"); }); it("orderByExample uses the first sortable non-compound field", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_object", object: "Account" }, makeDeps(), ); if (out.mode !== "describe_object") return; expect(Object.keys(out.object.orderByExample)).toHaveLength(1); }); it("requiredOnCreate excludes fields defaulted on create", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_object", object: "Account" }, makeDeps(), ); if (out.mode !== "describe_object") return; expect(out.object.requiredOnCreate).toEqual(["Name"]); }); it("throws when object is missing", async () => { await expect( buildDiscover({ org: ORG, mode: "describe_object" }, makeDeps()), ).rejects.toThrow(/requires "object"/); }); }); describe("describe_field mode (FR-11.3)", () => { it("returns the named field's metadata", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_field", object: "Account", field: "Industry" }, makeDeps(), ); expect(out.mode).toBe("describe_field"); if (out.mode !== "describe_field") return; expect(out.field.name).toBe("Industry"); expect(out.field.picklistValues).toEqual(["Technology", "Finance"]); }); it("throws when field is missing from spec", async () => { await expect( buildDiscover({ org: ORG, mode: "describe_field", object: "Account" }, makeDeps()), ).rejects.toThrow(/requires "field"/); }); it("throws when field is not on the object", async () => { await expect( buildDiscover( { org: ORG, mode: "describe_field", object: "Account", field: "DoesNotExist" }, makeDeps(), ), ).rejects.toThrow(/not found on "Account"/); }); }); // W-23336442: free-text org metadata (field label, picklist value, schema // description) is reflected verbatim through the SUCCESS envelope, whose // JSON.stringify escapes only C0 (U+0000-U+001F). DEL (U+007F) and the entire // Cf class (bidi overrides, zero-width, ...) survive raw, so buildDiscover must // neutralize them at the projection sites. `neutralizeControlChars` escapes to // a visible `\xNN` (cp<=0xff) or `\uNNNN` literal; ordinary Unicode is untouched. describe("free-text metadata neutralization (W-23336442)", () => { // Raw dangerous code points (escapes in SOURCE, raw at runtime): U+202E // RIGHT-TO-LEFT OVERRIDE (Cf), U+200B ZERO WIDTH SPACE (Cf), U+007F DELETE (Cc). const RLO = "\u{202e}"; const ZWSP = "\u{200b}"; const DEL = "\x7f"; // Their neutralized (visible, inert) forms. const ESC_RLO = "\\u202e"; const ESC_ZWSP = "\\u200b"; const ESC_DEL = "\\x7f"; const TAINTED_INFO: ObjectInfoResult = { ...ACCOUNT_INFO, fields: [ makeField({ apiName: "Industry", label: `Ind${RLO}ustry${ZWSP} café 日本語${DEL}`, dataType: "PICKLIST", createable: true, updateable: true, }), ], picklists: [ { apiName: "Industry", label: "Industry", required: false, values: [ { value: `Tech${RLO}nology${DEL}`, label: "Technology" }, { value: `Fin${ZWSP}ance café`, label: "Finance" }, ], }, ], }; function taintedDeps(): DiscoverDeps { return { primeDeps: makePrimeDeps(), getOrgAuth: async () => ({ alias: ORG, username: "u", instanceUrl: ORG_URL, accessToken: "t", orgId: "00D", }), getObjectInfo: async () => TAINTED_INFO, }; } it("escapes Cc/Cf in a field label but preserves ordinary Unicode (describe_field)", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_field", object: "Account", field: "Industry" }, taintedDeps(), ); if (out.mode !== "describe_field") return; // Escaped: the raw code points are ABSENT and appear as visible escapes. expect(out.field.label).toBe(`Ind${ESC_RLO}ustry${ESC_ZWSP} café 日本語${ESC_DEL}`); expect(out.field.label).not.toContain(RLO); expect(out.field.label).not.toContain(ZWSP); expect(out.field.label).not.toContain(DEL); // Ordinary Unicode survives verbatim. expect(out.field.label).toContain("café"); expect(out.field.label).toContain("日本語"); }); it("escapes Cc/Cf in picklist values but preserves ordinary Unicode (describe_field)", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_field", object: "Account", field: "Industry" }, taintedDeps(), ); if (out.mode !== "describe_field") return; expect(out.field.picklistValues).toEqual([ `Tech${ESC_RLO}nology${ESC_DEL}`, `Fin${ESC_ZWSP}ance café`, ]); const joined = (out.field.picklistValues ?? []).join(""); expect(joined).not.toContain(RLO); expect(joined).not.toContain(ZWSP); expect(joined).not.toContain(DEL); expect(joined).toContain("café"); }); it("escapes Cc/Cf in field labels reached via describe_object", async () => { const out = await buildDiscover( { org: ORG, mode: "describe_object", object: "Account" }, taintedDeps(), ); if (out.mode !== "describe_object") return; const industry = out.object.fields.find((f) => f.name === "Industry"); expect(industry?.label).toBe(`Ind${ESC_RLO}ustry${ESC_ZWSP} café 日本語${ESC_DEL}`); expect(industry?.label).not.toContain(RLO); expect(industry?.label).not.toContain(DEL); expect(industry?.picklistValues).toEqual([ `Tech${ESC_RLO}nology${ESC_DEL}`, `Fin${ESC_ZWSP}ance café`, ]); }); it("escapes Cc/Cf in the list_objects label (schema description)", async () => { const ALIAS = "test-discover-taint"; const URL = "https://test-discover-taint.my.salesforce.com"; // Descriptions carry the raw code points at runtime (source uses escapes). // U+202E/U+200B/U+007F are all valid GraphQL SourceCharacters (>= U+0020). const taintSchema = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { "Cust${RLO}omer${ZWSP} café 日本語${DEL}" Account(first: Int): AccountConnection! } type AccountConnection { edges: [AccountEdge!]! } type AccountEdge { node: Account! } type Account { Id: ID! } `); primeSchemaCache(ALIAS, taintSchema); primeSchemaCache(URL, taintSchema); const deps: DiscoverDeps = { primeDeps: { getOrgAuth: async () => ({ alias: ALIAS, username: "u", instanceUrl: URL, accessToken: "t", orgId: "00D", }), downloadSchema: async (auth) => { const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl); const filePath = path.join(schemaDir(), `${cacheKey}.json`); atomicWriteJson(filePath, { data: introspectionFromSchema(taintSchema) }); return { cacheKey, instanceUrl: auth.instanceUrl, typeCount: 0, downloadedAt: new Date().toISOString(), filePath, }; }, }, }; const out = await buildDiscover({ org: ALIAS, mode: "list_objects" }, deps); if (out.mode !== "list_objects") return; const account = out.objects.find((o) => o.name === "Account"); expect(account?.label).toBe(`Cust${ESC_RLO}omer${ESC_ZWSP} café 日本語${ESC_DEL}`); expect(account?.label).not.toContain(RLO); expect(account?.label).not.toContain(ZWSP); expect(account?.label).not.toContain(DEL); expect(account?.label).toContain("café"); expect(account?.label).toContain("日本語"); }); }); });