/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { beforeEach, describe, expect, it } from "vitest"; import { z } from "zod"; import { captureStdout } from "../../../__tests__/helpers/stdout.js"; import { AuthError } from "../../../lib/errors.js"; import { SchemaRefreshError } from "../../../lib/prime-schema.js"; import { PATH_MARKERS } from "../../../schemas/tool-adapter.js"; import { runMirror } from "../run-mirror.js"; const SCHEMA = z.object({ org: z.string().min(1), object: z.string() }); beforeEach(() => { // runMirror signals failure via process.exitCode; establish a known 0 // baseline so "success leaves it 0" is deterministic and a prior failing // case doesn't leak into the next. process.exitCode = 0; }); describe("runMirror", () => { it("prints the build output as one JSON line on success and leaves exitCode 0", async () => { const build = async (input: { org: string; object: string }) => ({ query: `# ${input.object}`, echoedOrg: input.org, }); const out = await captureStdout(() => runMirror('{"org":"ebikes","object":"Account"}', SCHEMA, build), ); expect(out.trim().split("\n")).toHaveLength(1); expect(JSON.parse(out)).toEqual({ query: "# Account", echoedOrg: "ebikes" }); expect(process.exitCode).toBe(0); }); it("passes the parsed+validated input to the build function", async () => { let received: unknown; const build = async (input: unknown) => { received = input; return {}; }; await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build)); expect(received).toEqual({ org: "o", object: "Account" }); }); it("emits an INVALID_ARGS envelope and sets exitCode 1 on malformed JSON", async () => { const build = async () => ({}); const out = await captureStdout(() => runMirror("{not json", SCHEMA, build)); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INVALID_ARGS"); expect(parsed.error.message).toBeTruthy(); expect(process.exitCode).toBe(1); }); it("emits an INVALID_ARGS envelope with Zod details on schema-validation failure", async () => { const build = async () => ({}); // org is "" → fails min(1); object missing const out = await captureStdout(() => runMirror('{"org":""}', SCHEMA, build)); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INVALID_ARGS"); expect(parsed.error.details).toBeTruthy(); expect(process.exitCode).toBe(1); }); it("does not call build when validation fails", async () => { let called = false; const build = async () => { called = true; return {}; }; await captureStdout(() => runMirror('{"org":""}', SCHEMA, build)); expect(called).toBe(false); }); it("reads JSON from stdin when no positional arg is given", async () => { const build = async (input: { org: string; object: string }) => ({ org: input.org }); const out = await captureStdout(() => runMirror(undefined, SCHEMA, build, { readStdin: async () => '{"org":"from-stdin","object":"Case"}', }), ); expect(JSON.parse(out)).toEqual({ org: "from-stdin" }); expect(process.exitCode).toBe(0); }); it("emits INVALID_ARGS without hanging when no arg is given and stdin is a TTY", async () => { let stdinRead = false; const build = async () => ({}); const out = await captureStdout(() => runMirror(undefined, SCHEMA, build, { isTTY: true, readStdin: async () => { stdinRead = true; return ""; }, }), ); expect(stdinRead).toBe(false); // guarded before touching stdin expect(JSON.parse(out).error.code).toBe("INVALID_ARGS"); expect(process.exitCode).toBe(1); }); it("maps a typed AuthError to AUTH_FAILED (W-23335328)", async () => { // A 401/403 introspection failure reclassified to AuthError must map to // AUTH_FAILED via the typed check. Its message contains "Schema priming // failed …", so classifyErrorMessage's message regex would return // SCHEMA_PRIME_FAILED — an untyped fallthrough would mislabel a 401 as a // schema problem, which is exactly the confusion W-23335328 fixes. const authMessage = 'Schema priming failed for "o" — the org session is expired or unauthorized. Re-authenticate with `sf org login web --alias o`.'; const build = async () => { throw new AuthError(authMessage); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("AUTH_FAILED"); expect(parsed.error.message).toContain("Re-authenticate"); expect(process.exitCode).toBe(1); }); it("the SAME message on a BARE Error would misclassify as SCHEMA_PRIME_FAILED — proves the typed check is load-bearing (W-23335328)", async () => { // Regression guard for the trap the typed AuthError branch avoids: the auth // message contains "Schema priming", which classifyErrorMessage's regex maps // to SCHEMA_PRIME_FAILED. Only the `instanceof AuthError` check rescues it. const build = async () => { throw new Error( 'Schema priming failed for "o" — the org session is expired or unauthorized. Re-authenticate with `sf org login web --alias o`.', ); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED"); }); it("maps SchemaRefreshError to SCHEMA_PRIME_FAILED", async () => { const build = async () => { throw new SchemaRefreshError("schema is stale", { instanceUrl: "https://x.my.salesforce.com", }); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("SCHEMA_PRIME_FAILED"); expect(parsed.error.message).toContain("stale"); expect(process.exitCode).toBe(1); }); it("maps an unrecognized thrown error to INTERNAL and preserves its message", async () => { const build = async () => { throw new Error("something totally unexpected happened"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INTERNAL"); expect(parsed.error.message).toContain("something totally unexpected"); expect(process.exitCode).toBe(1); }); it("best-effort classifies auth-shaped messages as AUTH_FAILED", async () => { const build = async () => { throw new Error("Authentication failed; no valid token for org"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("AUTH_FAILED"); expect(parsed.error.message).toContain("Authentication failed"); expect(process.exitCode).toBe(1); }); it("best-effort classifies a ~/.sf path message as AUTH_FAILED", async () => { const build = async () => { throw new Error("Could not read credentials from ~/.sf"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); expect(JSON.parse(out).error.code).toBe("AUTH_FAILED"); }); it("best-effort classifies introspection/priming messages as SCHEMA_PRIME_FAILED", async () => { const build = async () => { throw new Error("schema download failed during introspection"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED"); }); it("still prefers the typed SchemaRefreshError signal over message regex", async () => { // A SchemaRefreshError whose message happens to contain an auth-ish word // must still classify by type, not by the regex. const build = async () => { throw new SchemaRefreshError("stale; re-authorization may help", { instanceUrl: "https://x.my.salesforce.com", }); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED"); }); it("treats a literal '-' positional arg as 'read from stdin'", async () => { const build = async (input: { org: string; object: string }) => ({ org: input.org }); const out = await captureStdout(() => runMirror("-", SCHEMA, build, { readStdin: async () => '{"org":"via-dash","object":"Case"}', }), ); expect(JSON.parse(out)).toEqual({ org: "via-dash" }); expect(process.exitCode).toBe(0); }); it("does not leak a stack trace in the INTERNAL envelope by default", async () => { const build = async () => { throw new Error("boom"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INTERNAL"); expect(parsed.error.details).toBeUndefined(); }); it("attaches the stack to INTERNAL details when GRAPHITI_DEBUG=1", async () => { const prev = process.env.GRAPHITI_DEBUG; process.env.GRAPHITI_DEBUG = "1"; try { const build = async () => { throw new Error("boom"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INTERNAL"); expect(parsed.error.details.stack).toContain("boom"); } finally { if (prev === undefined) delete process.env.GRAPHITI_DEBUG; else process.env.GRAPHITI_DEBUG = prev; } }); // W-23336442 (N2): the mirror error path must SANITIZE the thrown message — // redact absolute filesystem paths (W-22697673) and neutralize Cc/Cf control // chars (bidi/zero-width/DEL that JSON.stringify leaves raw) — before it reaches // the host. Injected code points are `\u` escapes so the source has no raw bytes; // the trio U+202E/U+200B/U+007F is exactly what JSON.stringify does NOT escape. describe("W-23336442 N2 — error-path sanitization", () => { const BIDI = "\u{202e}"; const ZWSP = "\u{200b}"; const DEL = "\x7f"; it("neutralizes Cc/Cf control chars in the emitted message (not raw)", async () => { const build = async () => { throw new Error(`something ${BIDI}unexpected${ZWSP} happened${DEL}`); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INTERNAL"); // The specific injected code points must be ABSENT (escaped away), while // the ordinary text survives so the message stays debuggable. expect(parsed.error.message).not.toContain(BIDI); expect(parsed.error.message).not.toContain(ZWSP); expect(parsed.error.message).not.toContain(DEL); expect(parsed.error.message).toContain("something"); expect(parsed.error.message).toContain("unexpected"); expect(parsed.error.message).toContain("happened"); }); it("redacts an absolute filesystem path in the emitted message", async () => { const build = async () => { throw new Error("ENOENT: cannot open /Users/someone/secret/creds.json"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.message).not.toContain("/Users/someone/secret"); expect(parsed.error.message).toContain(PATH_MARKERS.redacted); }); it("sanitizes the GRAPHITI_DEBUG=1 stack so the opt-in debug frames leak no path", async () => { const prev = process.env.GRAPHITI_DEBUG; process.env.GRAPHITI_DEBUG = "1"; try { const build = async () => { // A hand-built stack carrying an absolute path frame — proves the // debug opt-in runs the stack through sanitizePaths too. const e = new Error("boom"); e.stack = "Error: boom\n at fn (/Users/someone/dev/repo/pkg/file.ts:1:1)"; throw e; }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); expect(parsed.error.code).toBe("INTERNAL"); expect(parsed.error.details.stack).toContain("boom"); expect(parsed.error.details.stack).not.toContain("/Users/someone/dev/repo"); } finally { if (prev === undefined) delete process.env.GRAPHITI_DEBUG; else process.env.GRAPHITI_DEBUG = prev; } }); it("still classifies a ~/.sf auth message as AUTH_FAILED (code mapping intact)", async () => { const build = async () => { throw new Error("Could not read credentials from ~/.sf"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); // Basic smoke: an auth-keyworded message routes to AUTH_FAILED. Note this // input alone can't prove classification reads the RAW vs. the sanitized // text — "~/.sf" is already relative (single segment, no leading "/"), so // sanitizePaths leaves it byte-for-byte unchanged. The next test pins the // raw-vs-sanitized choice with a keyword that ONLY survives in the raw. expect(JSON.parse(out).error.code).toBe("AUTH_FAILED"); }); it("classifies AUTH from the RAW message even when the auth keyword sits in a path that redaction eats", async () => { // The load-bearing case for `classifyErrorMessage(rawMessage)` (run-mirror.ts): // the auth hint lives INSIDE an absolute path (…/auth-service/…). Classify on // the raw string → `\bauth\b` matches → AUTH_FAILED. But sanitizePaths redacts // the whole path to ``, deleting the keyword — so classifying on the // SANITIZED text would fall through to INTERNAL. Swapping the classify input // to the sanitized `message` would silently regress this, and only this test // would catch it (the ~/.sf smoke above is sanitize-invariant). const build = async () => { throw new Error("Could not read token /var/lib/auth-service/creds.db"); }; const out = await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build), ); const parsed = JSON.parse(out); // Classified on the RAW message (keyword still present there). expect(parsed.error.code).toBe("AUTH_FAILED"); // Emitted message is the SANITIZED one: the path — and with it the "auth" // keyword — is redacted, so the two inputs to classification truly diverge. expect(parsed.error.message).toContain(PATH_MARKERS.redacted); expect(parsed.error.message).not.toContain("auth"); }); }); });