@salesforce/graphiti 10.10.2 → 10.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +23 -0
- package/CHANGELOG.md +10 -0
- package/README.md +17 -6
- package/dist/commands/connect.d.ts +2 -1
- package/dist/commands/connect.js +28 -7
- package/dist/commands/connect.js.map +1 -1
- package/dist/intent/build-connect.d.ts +24 -0
- package/dist/intent/build-connect.js +47 -0
- package/dist/intent/build-connect.js.map +1 -0
- package/dist/intent/types.d.ts +23 -0
- package/dist/lib/fs-utils.d.ts +3 -1
- package/dist/lib/fs-utils.js +7 -3
- package/dist/lib/fs-utils.js.map +1 -1
- package/dist/lib/introspect.js +20 -1
- package/dist/lib/introspect.js.map +1 -1
- package/dist/lib/prime-schema.d.ts +70 -16
- package/dist/lib/prime-schema.js +166 -33
- package/dist/lib/prime-schema.js.map +1 -1
- package/dist/lib/walker.d.ts +10 -0
- package/dist/lib/walker.js +26 -2
- package/dist/lib/walker.js.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/sf-gql-connect.d.ts +9 -0
- package/dist/mcp/tools/sf-gql-connect.js +27 -0
- package/dist/mcp/tools/sf-gql-connect.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/object-info.ts +37 -0
- package/src/commands/__tests__/connect.spec.ts +92 -0
- package/src/commands/connect.ts +28 -6
- package/src/intent/__tests__/build-connect.spec.ts +103 -0
- package/src/intent/build-connect.ts +55 -0
- package/src/intent/types.ts +25 -0
- package/src/lib/__tests__/introspect.spec.ts +34 -0
- package/src/lib/__tests__/prime-schema.spec.ts +341 -0
- package/src/lib/__tests__/walker.spec.ts +13 -0
- package/src/lib/fs-utils.ts +8 -3
- package/src/lib/introspect.ts +29 -6
- package/src/lib/prime-schema.ts +184 -32
- package/src/lib/walker.ts +26 -2
- package/src/mcp/__tests__/server.spec.ts +1 -0
- package/src/mcp/server.ts +2 -0
- package/src/mcp/tools/__tests__/sf-gql-connect.spec.ts +202 -0
- package/src/mcp/tools/sf-gql-connect.ts +42 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { buildSchema } from "graphql";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
|
|
13
|
+
import { captureStdout } from "../../__tests__/helpers/stdout.js";
|
|
14
|
+
import { schemaCacheKeyForInstanceUrl, schemaDir } from "../../lib/introspect.js";
|
|
15
|
+
import { type PrimeDeps, SchemaRefreshError } from "../../lib/prime-schema.js";
|
|
16
|
+
import { connectCommand } from "../connect.js";
|
|
17
|
+
|
|
18
|
+
const ORG = "test-connect-cli";
|
|
19
|
+
const ORG_URL = "https://test-connect-cli.my.salesforce.com";
|
|
20
|
+
const SCHEMA = buildSchema(`type Query { x: Int }`);
|
|
21
|
+
|
|
22
|
+
describe("commands/connect", () => {
|
|
23
|
+
let tmpRoot: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-cmd-connect-"));
|
|
27
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
delete process.env.GRAPHITI_HOME;
|
|
32
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function schemaFile(): string {
|
|
36
|
+
return path.join(schemaDir(), `${schemaCacheKeyForInstanceUrl(ORG_URL)}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it("--refresh routes through the shared path and clears the on-disk ObjectInfo cache", async () => {
|
|
40
|
+
const deps = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
41
|
+
await captureStdout(() => connectCommand(ORG, {}, deps)); // initial prime
|
|
42
|
+
|
|
43
|
+
// Seed an ObjectInfo disk cache entry for this org — the stale data a
|
|
44
|
+
// refresh must clear (the bug this story fixes).
|
|
45
|
+
const oiDir = path.join(tmpRoot, "cache", "objectInfos", ORG);
|
|
46
|
+
fs.mkdirSync(oiDir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(oiDir, "Account.json"), "{}");
|
|
48
|
+
expect(fs.existsSync(oiDir)).toBe(true);
|
|
49
|
+
|
|
50
|
+
const out = await captureStdout(() => connectCommand(ORG, { refresh: true }, deps));
|
|
51
|
+
expect(fs.existsSync(oiDir)).toBe(false); // cleared by the refresh
|
|
52
|
+
expect(out).toMatch(/Refreshed test-connect-cli/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("--refresh failure throws SchemaRefreshError and keeps the cached schema", async () => {
|
|
56
|
+
const good = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
57
|
+
await captureStdout(() => connectCommand(ORG, {}, good)); // prime a real cache
|
|
58
|
+
|
|
59
|
+
const before = fs.readFileSync(schemaFile(), "utf-8");
|
|
60
|
+
const failing: PrimeDeps = {
|
|
61
|
+
getOrgAuth: good.getOrgAuth,
|
|
62
|
+
downloadSchema: async () => {
|
|
63
|
+
throw new Error("introspection failed");
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await expect(
|
|
68
|
+
captureStdout(() => connectCommand(ORG, { refresh: true }, failing)),
|
|
69
|
+
).rejects.toBeInstanceOf(SchemaRefreshError);
|
|
70
|
+
|
|
71
|
+
// The previously cached schema is left intact.
|
|
72
|
+
expect(fs.readFileSync(schemaFile(), "utf-8")).toBe(before);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("connect without --refresh on a cached org reports 'already cached' and does not download", async () => {
|
|
76
|
+
const good = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
77
|
+
await captureStdout(() => connectCommand(ORG, {}, good)); // prime
|
|
78
|
+
|
|
79
|
+
let downloadCalls = 0;
|
|
80
|
+
const counting: PrimeDeps = {
|
|
81
|
+
getOrgAuth: good.getOrgAuth,
|
|
82
|
+
downloadSchema: async (a) => {
|
|
83
|
+
downloadCalls++;
|
|
84
|
+
return good.downloadSchema(a);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const out = await captureStdout(() => connectCommand(ORG, {}, counting));
|
|
89
|
+
expect(out).toMatch(/already cached/);
|
|
90
|
+
expect(downloadCalls).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/commands/connect.ts
CHANGED
|
@@ -5,18 +5,23 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getOrgAuth } from "../lib/auth.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getSchemaMetadata } from "../lib/introspect.js";
|
|
9
|
+
import { type PrimeDeps, primeSchemaWithLock } from "../lib/prime-schema.js";
|
|
9
10
|
|
|
10
11
|
export async function connectCommand(
|
|
11
12
|
orgAlias: string,
|
|
12
13
|
opts: { refresh?: boolean } = {},
|
|
14
|
+
deps?: PrimeDeps,
|
|
13
15
|
): Promise<void> {
|
|
16
|
+
const getAuth = deps?.getOrgAuth ?? getOrgAuth;
|
|
14
17
|
console.log(`Connecting to ${orgAlias}...`);
|
|
15
18
|
|
|
16
|
-
const auth = await
|
|
19
|
+
const auth = await getAuth(orgAlias);
|
|
17
20
|
console.log(`Authenticated as ${auth.username} on ${auth.instanceUrl}`);
|
|
18
21
|
|
|
19
22
|
if (!opts.refresh) {
|
|
23
|
+
// Resolve via the already-resolved instance URL (not the alias) so we
|
|
24
|
+
// don't trigger a second org resolution, and so injected deps stay honored.
|
|
20
25
|
const existing = getSchemaMetadata(auth.instanceUrl);
|
|
21
26
|
if (existing) {
|
|
22
27
|
const age = formatSchemaAge(new Date(existing.downloadedAt));
|
|
@@ -26,21 +31,38 @@ export async function connectCommand(
|
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
const verb = opts.refresh ? "Refreshing" : "Downloading";
|
|
35
|
+
console.log(`${verb} GraphQL schema via introspection...`);
|
|
30
36
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
31
37
|
let i = 0;
|
|
32
38
|
const startTime = Date.now();
|
|
33
39
|
const spinner = setInterval(() => {
|
|
34
40
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
35
|
-
process.stderr.write(`\r${frames[i++ % frames.length]}
|
|
41
|
+
process.stderr.write(`\r${frames[i++ % frames.length]} ${verb} schema... (${elapsed}s)`);
|
|
36
42
|
}, 100);
|
|
37
43
|
|
|
38
44
|
try {
|
|
39
|
-
|
|
45
|
+
// Route through the shared priming layer so a refresh coherently clears
|
|
46
|
+
// all three caches (introspection JSON, in-memory schema, ObjectInfo) and
|
|
47
|
+
// coalesces with any concurrent CLI/MCP refresh. A failed refresh throws
|
|
48
|
+
// SchemaRefreshError (kept caches + staleness message), surfaced by cli.ts.
|
|
49
|
+
const result = await primeSchemaWithLock(orgAlias, deps, { forceRefresh: !!opts.refresh });
|
|
40
50
|
clearInterval(spinner);
|
|
41
51
|
process.stderr.write("\r");
|
|
42
52
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
43
|
-
|
|
53
|
+
// `result.typeCount` is set on the download path; a coalesced refresh
|
|
54
|
+
// (no download performed here) falls back to a one-time metadata read.
|
|
55
|
+
const typeCount = result.typeCount ?? getSchemaMetadata(auth.instanceUrl)?.typeCount ?? 0;
|
|
56
|
+
if (opts.refresh && !result.refreshed) {
|
|
57
|
+
// A concurrent refresh produced the new schema while we waited.
|
|
58
|
+
console.log(
|
|
59
|
+
`Schema for ${orgAlias} was just refreshed by a concurrent process: ${typeCount} types.`,
|
|
60
|
+
);
|
|
61
|
+
} else if (opts.refresh) {
|
|
62
|
+
console.log(`Refreshed ${orgAlias}. Schema cached: ${typeCount} types (${elapsed}s).`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`Connected to ${orgAlias}. Schema cached: ${typeCount} types (${elapsed}s).`);
|
|
65
|
+
}
|
|
44
66
|
} catch (err) {
|
|
45
67
|
clearInterval(spinner);
|
|
46
68
|
process.stderr.write("\r");
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { buildSchema } from "graphql";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
|
|
13
|
+
import { type PrimeDeps, SchemaRefreshError } from "../../lib/prime-schema.js";
|
|
14
|
+
import { buildConnect } from "../build-connect.js";
|
|
15
|
+
|
|
16
|
+
const ORG = "test-connect";
|
|
17
|
+
const ORG_URL = "https://test-connect.my.salesforce.com";
|
|
18
|
+
const SCHEMA = buildSchema(`type Query { x: Int }`);
|
|
19
|
+
|
|
20
|
+
function failingPrimeDeps(err: unknown): PrimeDeps {
|
|
21
|
+
return {
|
|
22
|
+
getOrgAuth: async () => ({
|
|
23
|
+
alias: ORG,
|
|
24
|
+
username: "u",
|
|
25
|
+
instanceUrl: ORG_URL,
|
|
26
|
+
accessToken: "t",
|
|
27
|
+
orgId: "00D",
|
|
28
|
+
}),
|
|
29
|
+
downloadSchema: async () => {
|
|
30
|
+
throw err;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("intent/build-connect", () => {
|
|
36
|
+
let tmpRoot: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-build-connect-"));
|
|
40
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
delete process.env.GRAPHITI_HOME;
|
|
45
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("lazily primes on first connect (cached: false, refreshed: false)", async () => {
|
|
49
|
+
const out = await buildConnect(
|
|
50
|
+
{ org: ORG },
|
|
51
|
+
{ primeDeps: makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA) },
|
|
52
|
+
);
|
|
53
|
+
expect(out.cached).toBe(false);
|
|
54
|
+
expect(out.refreshed).toBe(false);
|
|
55
|
+
expect(out.instanceUrl).toBe(ORG_URL);
|
|
56
|
+
expect(out.warnings).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns cached when a connect finds an existing schema and forceRefresh is omitted", async () => {
|
|
60
|
+
const primeDeps = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
61
|
+
await buildConnect({ org: ORG }, { primeDeps }); // prime
|
|
62
|
+
const out = await buildConnect({ org: ORG }, { primeDeps });
|
|
63
|
+
expect(out.cached).toBe(true);
|
|
64
|
+
expect(out.refreshed).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("forceRefresh re-downloads and reports refreshed: true", async () => {
|
|
68
|
+
const primeDeps = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
69
|
+
await buildConnect({ org: ORG }, { primeDeps }); // prime
|
|
70
|
+
const out = await buildConnect({ org: ORG, forceRefresh: true }, { primeDeps });
|
|
71
|
+
expect(out.refreshed).toBe(true);
|
|
72
|
+
expect(out.cached).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("refresh failure with a surviving cache returns a soft staleness warning", async () => {
|
|
76
|
+
// Prime a real cache first, then fail the refresh.
|
|
77
|
+
await buildConnect({ org: ORG }, { primeDeps: makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA) });
|
|
78
|
+
|
|
79
|
+
const out = await buildConnect(
|
|
80
|
+
{ org: ORG, forceRefresh: true },
|
|
81
|
+
{
|
|
82
|
+
primeDeps: failingPrimeDeps(new Error("introspection failed")),
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(out.refreshed).toBe(false);
|
|
87
|
+
expect(out.cached).toBe(true);
|
|
88
|
+
expect(out.instanceUrl).toBe(ORG_URL);
|
|
89
|
+
expect(out.durationMs).toBe(0);
|
|
90
|
+
expect(out.warnings?.[0]).toMatch(/keeping the previously cached schema/i);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("refresh failure with no prior cache propagates the error", async () => {
|
|
94
|
+
await expect(
|
|
95
|
+
buildConnect(
|
|
96
|
+
{ org: ORG, forceRefresh: true },
|
|
97
|
+
{
|
|
98
|
+
primeDeps: failingPrimeDeps(new Error("introspection failed")),
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
).rejects.toBeInstanceOf(SchemaRefreshError);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type ConnectOutput, type ConnectSpec } from "./types.js";
|
|
8
|
+
import { type PrimeDeps, primeSchemaWithLock, SchemaRefreshError } from "../lib/prime-schema.js";
|
|
9
|
+
|
|
10
|
+
export interface ConnectDeps {
|
|
11
|
+
primeDeps?: PrimeDeps;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the `sf_gql_connect` payload (W-22845606). Primes the schema cache for
|
|
16
|
+
* `spec.org`, or — with `spec.forceRefresh` — re-downloads it and coherently
|
|
17
|
+
* clears all three caches (on-disk introspection JSON, in-memory parsed schema,
|
|
18
|
+
* ObjectInfo).
|
|
19
|
+
*
|
|
20
|
+
* Failure policy (Q6 of the plan): a failed *refresh* that leaves a usable
|
|
21
|
+
* cached schema behind is recoverable, so we return a soft `ConnectOutput`
|
|
22
|
+
* (`refreshed: false`, `cached: true`) carrying a staleness-aware `warnings[]`
|
|
23
|
+
* entry — the agent can keep working on the still-valid schema. A failure with
|
|
24
|
+
* no prior cache (or any non-refresh failure such as missing auth) propagates,
|
|
25
|
+
* so the MCP SDK surfaces it as a hard tool error.
|
|
26
|
+
*/
|
|
27
|
+
export async function buildConnect(
|
|
28
|
+
spec: ConnectSpec,
|
|
29
|
+
deps: ConnectDeps = {},
|
|
30
|
+
): Promise<ConnectOutput> {
|
|
31
|
+
try {
|
|
32
|
+
const prime = await primeSchemaWithLock(spec.org, deps.primeDeps, {
|
|
33
|
+
forceRefresh: spec.forceRefresh,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
org: spec.org,
|
|
37
|
+
instanceUrl: prime.instanceUrl,
|
|
38
|
+
refreshed: prime.refreshed,
|
|
39
|
+
cached: prime.cached,
|
|
40
|
+
durationMs: prime.durationMs,
|
|
41
|
+
};
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e instanceof SchemaRefreshError && e.staleSince) {
|
|
44
|
+
return {
|
|
45
|
+
org: spec.org,
|
|
46
|
+
instanceUrl: e.instanceUrl,
|
|
47
|
+
refreshed: false,
|
|
48
|
+
cached: true,
|
|
49
|
+
durationMs: 0,
|
|
50
|
+
warnings: [e.message],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/intent/types.ts
CHANGED
|
@@ -164,6 +164,31 @@ export interface DeleteSpec {
|
|
|
164
164
|
operationName?: string;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export interface ConnectSpec {
|
|
168
|
+
org: string;
|
|
169
|
+
/**
|
|
170
|
+
* Re-download the schema even if it is already cached, coherently clearing
|
|
171
|
+
* all three caches (on-disk introspection JSON, in-memory parsed schema, and
|
|
172
|
+
* ObjectInfo). Defaults to false — a plain connect lazily primes on miss.
|
|
173
|
+
*/
|
|
174
|
+
forceRefresh?: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ConnectOutput {
|
|
178
|
+
org: string;
|
|
179
|
+
instanceUrl: string;
|
|
180
|
+
/** True when this call performed a forced re-download. */
|
|
181
|
+
refreshed: boolean;
|
|
182
|
+
/** True when an existing cache satisfied the request (no download performed). */
|
|
183
|
+
cached: boolean;
|
|
184
|
+
durationMs: number;
|
|
185
|
+
/**
|
|
186
|
+
* Populated when a refresh failed but a usable cached schema survived — the
|
|
187
|
+
* staleness-aware message tells the caller the cache is still valid.
|
|
188
|
+
*/
|
|
189
|
+
warnings?: string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
export type RawOperation = "query" | "mutation" | "aggregate";
|
|
168
193
|
|
|
169
194
|
export interface RawSpec {
|
|
@@ -376,6 +376,40 @@ describe("introspect", () => {
|
|
|
376
376
|
}
|
|
377
377
|
});
|
|
378
378
|
|
|
379
|
+
it("bounds the introspection with a timeout and opts the POST into one retry (W-22845606)", async () => {
|
|
380
|
+
const core = (await import("@salesforce/core")) as unknown as {
|
|
381
|
+
__request: ReturnType<typeof vi.fn>;
|
|
382
|
+
};
|
|
383
|
+
core.__request.mockClear();
|
|
384
|
+
core.__request.mockResolvedValueOnce({ data: { __schema: { types: [] } } });
|
|
385
|
+
|
|
386
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-dl-opts-"));
|
|
387
|
+
const prevHome = process.env.GRAPHITI_HOME;
|
|
388
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
389
|
+
try {
|
|
390
|
+
await downloadSchema(auth);
|
|
391
|
+
// connection.request(requestInfo, options) — the 2nd arg bounds the
|
|
392
|
+
// request (jsforce defaults to 30 min) and opts POST into retry
|
|
393
|
+
// (jsforce skips POST by default).
|
|
394
|
+
const options = core.__request.mock.calls[0][1] as {
|
|
395
|
+
timeout?: number;
|
|
396
|
+
retry?: { methods?: string[]; maxRetries?: number; statusCodes?: number[] };
|
|
397
|
+
};
|
|
398
|
+
expect(typeof options?.timeout).toBe("number");
|
|
399
|
+
expect(options.timeout as number).toBeGreaterThan(0);
|
|
400
|
+
expect(options?.retry?.methods).toContain("POST");
|
|
401
|
+
// Pin the transient-failure contract (W-22845606): exactly one retry on
|
|
402
|
+
// the listed 5xx/429/420 codes — jsforce's defaults (maxRetries 5, POST
|
|
403
|
+
// not retried) would otherwise apply, and value drift here is invisible
|
|
404
|
+
// to tsc/eslint since it's a plain object literal.
|
|
405
|
+
expect(options?.retry?.maxRetries).toBe(1);
|
|
406
|
+
expect(options?.retry?.statusCodes).toEqual([420, 429, 500, 502, 503, 504]);
|
|
407
|
+
} finally {
|
|
408
|
+
process.env.GRAPHITI_HOME = prevHome;
|
|
409
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
379
413
|
it("throws when the response contains GraphQL errors", async () => {
|
|
380
414
|
const core = (await import("@salesforce/core")) as unknown as {
|
|
381
415
|
__request: ReturnType<typeof vi.fn>;
|