/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import fs from "fs"; import os from "os"; import path from "path"; import { describe, expect, it, vi } from "vitest"; import { downloadSchema, getSchemaFilePath, normalizeInstanceUrl, schemaExists, schemaCacheKeyForInstanceUrl, stripDataCloudTypes, } from "../introspect.js"; vi.mock("@salesforce/core", () => { const request = vi.fn().mockResolvedValue({ data: { ok: true } }); return { Org: { create: vi.fn().mockResolvedValue({ getConnection: () => ({ getApiVersion: () => "67.0", request }), }), }, __request: request, }; }); describe("introspect", () => { describe("schema cache keys", () => { it("are derived from normalized instance URLs", () => { const first = "https://Example.My.Salesforce.com/"; const second = "https://example.my.salesforce.com"; const third = "https://different.my.salesforce.com"; expect(normalizeInstanceUrl(first)).toBe("https://example.my.salesforce.com"); expect(schemaCacheKeyForInstanceUrl(first)).toBe(schemaCacheKeyForInstanceUrl(second)); expect(schemaCacheKeyForInstanceUrl(first)).not.toBe(schemaCacheKeyForInstanceUrl(third)); }); }); describe("stripDataCloudTypes", () => { function makeTypeRef(name: string, kind = "OBJECT") { return { kind, name, ofType: null }; } function makeWrappedTypeRef(name: string) { return { kind: "NON_NULL", name: null, ofType: { kind: "OBJECT", name, ofType: null } }; } function makeIntrospectionResult(types: any[]) { return { data: { __schema: { types } } }; } it("removes __dlm types and references", () => { const raw = makeIntrospectionResult([ { name: "Query", kind: "OBJECT", fields: [ { name: "Account", type: makeTypeRef("AccountConnection") }, { name: "ssot__Account__dlm", type: makeTypeRef("ssot__Account__dlmConnection") }, { name: "Case", type: makeTypeRef("CaseConnection") }, ], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "AccountConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__Account__dlmConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__Account__dlm", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "CaseConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, ]); const { result, removedCount } = stripDataCloudTypes(raw); const types = result.data.__schema.types; const typeNames = types.map((t: any) => t.name); expect(removedCount).toBe(2); expect(typeNames).not.toContain("ssot__Account__dlm"); expect(typeNames).not.toContain("ssot__Account__dlmConnection"); expect(typeNames).toContain("AccountConnection"); expect(typeNames).toContain("CaseConnection"); const queryType = types.find((t: any) => t.name === "Query"); const queryFieldNames = queryType.fields.map((f: any) => f.name); expect(queryFieldNames).not.toContain("ssot__Account__dlm"); expect(queryFieldNames).toContain("Account"); expect(queryFieldNames).toContain("Case"); }); it("removes ssot__ prefixed types", () => { const raw = makeIntrospectionResult([ { name: "Query", kind: "OBJECT", fields: [ { name: "Account", type: makeTypeRef("AccountConnection") }, { name: "ssot__SomeEntity", type: makeTypeRef("ssot__SomeEntityConnection") }, ], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "AccountConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__SomeEntityConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__SomeEntity", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, ]); const { result, removedCount } = stripDataCloudTypes(raw); const typeNames = result.data.__schema.types.map((t: any) => t.name); expect(removedCount).toBe(2); expect(typeNames).not.toContain("ssot__SomeEntity"); expect(typeNames).not.toContain("ssot__SomeEntityConnection"); expect(typeNames).toContain("AccountConnection"); }); it("handles wrapped type refs (NON_NULL)", () => { const raw = makeIntrospectionResult([ { name: "Query", kind: "OBJECT", fields: [ { name: "Account", type: makeWrappedTypeRef("AccountConnection") }, { name: "ssot__Lead__dlm", type: makeWrappedTypeRef("ssot__Lead__dlmConnection") }, ], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "AccountConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__Lead__dlmConnection", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__Lead__dlm", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, ]); const { result } = stripDataCloudTypes(raw); const queryType = result.data.__schema.types.find((t: any) => t.name === "Query"); expect(queryType.fields).toHaveLength(1); expect(queryType.fields[0].name).toBe("Account"); }); it("cleans up possibleTypes on unions", () => { const raw = makeIntrospectionResult([ { name: "RecordUnion", kind: "UNION", fields: null, inputFields: null, interfaces: null, enumValues: null, possibleTypes: [ { kind: "OBJECT", name: "Account" }, { kind: "OBJECT", name: "ssot__Foo__dlm" }, ], }, { name: "Account", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "ssot__Foo__dlm", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, ]); const { result } = stripDataCloudTypes(raw); const union = result.data.__schema.types.find((t: any) => t.name === "RecordUnion"); expect(union.possibleTypes).toHaveLength(1); expect(union.possibleTypes[0].name).toBe("Account"); }); it("is a no-op when no DLM/SSOT types exist", () => { const raw = makeIntrospectionResult([ { name: "Query", kind: "OBJECT", fields: [{ name: "Account", type: makeTypeRef("Account") }], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, { name: "Account", kind: "OBJECT", fields: [], inputFields: null, interfaces: [], enumValues: null, possibleTypes: null, }, ]); const { result, removedCount } = stripDataCloudTypes(raw); expect(removedCount).toBe(0); expect(result.data.__schema.types).toHaveLength(2); }); }); describe("schema helpers take instanceUrl (no auth)", () => { it("getSchemaFilePath derives a stable path from a normalized URL", () => { const a = getSchemaFilePath("https://Example.My.salesforce.com/"); const b = getSchemaFilePath("https://example.my.salesforce.com"); expect(a).toBe(b); // normalization: trailing slash + case ignored expect(a.endsWith(".json")).toBe(true); }); it("schemaExists is false for an unknown URL", () => { expect(schemaExists("https://nope.my.salesforce.com")).toBe(false); }); }); describe("executeGraphQL", () => { it("posts via connection.request with the org API version", async () => { const { executeGraphQL } = await import("../introspect.js"); const core = (await import("@salesforce/core")) as unknown as { __request: ReturnType; }; core.__request.mockClear(); const auth = { alias: "o", username: "u", instanceUrl: "https://o.my.salesforce.com", accessToken: "t", orgId: "00D", }; const result = await executeGraphQL(auth as never, "query { x }", { a: 1 }); expect(result).toEqual({ data: { ok: true } }); expect(core.__request).toHaveBeenCalledWith( expect.objectContaining({ method: "POST", url: expect.stringContaining("/services/data/v67.0/graphql"), body: JSON.stringify({ query: "query { x }", variables: { a: 1 } }), headers: expect.objectContaining({ "Content-Type": "application/json" }), }), ); }); }); describe("downloadSchema", () => { const auth = { alias: "o", username: "u", instanceUrl: "https://o.my.salesforce.com", accessToken: "t", orgId: "00D", } as never; it("happy path: writes cache file and returns correct metadata", async () => { const core = (await import("@salesforce/core")) as unknown as { __request: ReturnType; }; core.__request.mockClear(); core.__request.mockResolvedValueOnce({ data: { __schema: { types: [ { name: "Account", kind: "OBJECT" }, { name: "__Type", kind: "SCALAR" }, ], }, }, }); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-dl-")); const prevHome = process.env.GRAPHITI_HOME; process.env.GRAPHITI_HOME = tmpRoot; try { const result = await downloadSchema(auth); expect(result.instanceUrl).toBe("https://o.my.salesforce.com"); expect(result.filePath.endsWith(".json")).toBe(true); expect(fs.existsSync(result.filePath)).toBe(true); // Only "Account" counts — "__Type" starts with "__" expect(result.typeCount).toBe(1); } finally { process.env.GRAPHITI_HOME = prevHome; fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); it("bounds the introspection with a timeout and opts the POST into one retry (W-22845606)", async () => { const core = (await import("@salesforce/core")) as unknown as { __request: ReturnType; }; core.__request.mockClear(); core.__request.mockResolvedValueOnce({ data: { __schema: { types: [] } } }); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-dl-opts-")); const prevHome = process.env.GRAPHITI_HOME; process.env.GRAPHITI_HOME = tmpRoot; try { await downloadSchema(auth); // connection.request(requestInfo, options) — the 2nd arg bounds the // request (jsforce defaults to 30 min) and opts POST into retry // (jsforce skips POST by default). const options = core.__request.mock.calls[0][1] as { timeout?: number; retry?: { methods?: string[]; maxRetries?: number; statusCodes?: number[] }; }; expect(typeof options?.timeout).toBe("number"); expect(options.timeout as number).toBeGreaterThan(0); expect(options?.retry?.methods).toContain("POST"); // Pin the transient-failure contract (W-22845606): exactly one retry on // the listed 5xx/429/420 codes — jsforce's defaults (maxRetries 5, POST // not retried) would otherwise apply, and value drift here is invisible // to tsc/eslint since it's a plain object literal. expect(options?.retry?.maxRetries).toBe(1); expect(options?.retry?.statusCodes).toEqual([420, 429, 500, 502, 503, 504]); } finally { process.env.GRAPHITI_HOME = prevHome; fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); it("throws when the response contains GraphQL errors", async () => { const core = (await import("@salesforce/core")) as unknown as { __request: ReturnType; }; core.__request.mockClear(); core.__request.mockResolvedValueOnce({ errors: [{ message: "boom" }] }); await expect(downloadSchema(auth)).rejects.toThrow(/Introspection query returned errors/); }); it("throws when the response has no __schema field", async () => { const core = (await import("@salesforce/core")) as unknown as { __request: ReturnType; }; core.__request.mockClear(); core.__request.mockResolvedValueOnce({ data: {} }); await expect(downloadSchema(auth)).rejects.toThrow(/did not return a __schema/); }); }); });