/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { buildSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { createSession, type FieldProjectionNode, selectLeaf } from "../../lib/session.js"; import { buildOutput } from "../build-output.js"; const schema = buildSchema(` type Query { ping: String } `); const argSchema = buildSchema(` type Query { accounts(first: Int): String } `); describe("intent/build-output", () => { it("renders query, no warnings on valid query", () => { const session = createSession("test", "query"); selectLeaf(session, ["ping"]); const out = buildOutput(session, schema); expect(out.query).toMatch(/ping/); expect(out.warnings).toEqual([]); expect(typeof out.types).toBe("string"); }); it("threads priming note into warnings", () => { const session = createSession("test", "query"); selectLeaf(session, ["ping"]); const out = buildOutput(session, schema, 'Note: Primed schema cache for "a" (took 12ms)'); expect(out.warnings).toHaveLength(1); expect(out.warnings[0]!).toMatch(/Primed schema cache/); }); it("emits Validation: prefix for query-level errors", () => { const session = createSession("test", "query"); selectLeaf(session, ["doesNotExist"]); const out = buildOutput(session, schema); expect(out.warnings.some((w) => w.startsWith("Validation:"))).toBe(true); }); it("surfaces codegen failure as Codegen: warning + placeholder types", () => { const session = createSession("test", "query"); selectLeaf(session, ["ping"]); // Trigger: assign non-string values to session.id and session.name so that // generateTypes' internal toPascalCase call throws. If codegen ever guards // these reads with String(...), this trigger will stop firing and the test // will fail loudly — replace the trigger with another path that makes // generateTypes throw (e.g., an unsupported codegen language option). (session as unknown as { id: unknown }).id = { broken: true }; (session as unknown as { name: unknown }).name = { broken: true }; const out = buildOutput(session, schema); expect(out.types).toMatch(/\/\/ Type generation failed:/); expect(out.warnings.some((w) => w.startsWith("Codegen:"))).toBe(true); }); // W-23204027 (PR #694 review): buildOutput's docstring no longer claims "Never // throws". renderQuery runs OUTSIDE the try/catch and enforces the render-layer // fail-safe, so a hostile identifier/arg-key that slipped past the per-builder // guards throws instead of emitting an injectable name. This is caught and // classified UserInput at the MCP boundary (runTool), but direct callers must // be prepared for it — and it must NOT be swallowed into warnings[], which would // let an injected selection reach the output. This test pins that contract. it("propagates the render-layer fail-safe throw (does not swallow it into warnings)", () => { const session = createSession("test", "query"); selectLeaf(session, ["accounts"]); const node = session.nodes.find( (n): n is FieldProjectionNode => n.kind === "field" && n.fieldName === "accounts", )!; // A raw, injectable argument key (as sf_gql_raw's `set @args/` // could plant) — never a valid GraphQL Name. node.args["first) { stolen } evil("] = "1"; expect(() => buildOutput(session, argSchema)).toThrow(/is not a valid GraphQL Name/); }); });