When presets are checked and custom scopes entered in the same submit, the workloads already fan out through addWorkloads, so the legacy addGraph call now carries only the custom scopes. Empty presetIds with custom scopes no longer falls back to the default workload selection.
Unified · Split
| @@ -7,6 +7,7 @@ import type { ReactElement, ReactNode } from "react"; · 6 unmodified lines | ||
| 7 | 7 | import { |
| 8 | 8 | MicrosoftWorkloadResultPanel, |
| 9 | 9 | microsoftAddWorkloadsPayload, |
| 10 | + microsoftCustomGraphPayload, | |
| 10 | 11 | submitMicrosoftWorkloadsSelection, |
| 11 | 12 | type AddMicrosoftWorkloadsMutation, |
| 12 | 13 | } from "./AddMicrosoftSource"; |
| @@ -169,4 +170,25 @@ describe("AddMicrosoftSource per-workload submit", () => { · 156 unmodified lines | ||
| 169 | 170 | ], |
| 170 | 171 | }); |
| 171 | 172 | }); |
| 173 | + | |
| 174 | + it("mixed flow: the custom Graph payload carries only custom scopes, never the fanned-out presets", () => { | |
| 175 | + // Checked workloads go through addWorkloads as their own integrations in | |
| 176 | + // the same submit; if the custom addGraph payload repeated their preset | |
| 177 | + // ids, the custom bundle would duplicate the same tools. | |
| 178 | + expect( | |
| 179 | + microsoftCustomGraphPayload({ | |
| 180 | + customScopes: ["Sites.Read.All", "Custom.Scope"], | |
| 181 | + slug: "microsoft_graph_custom", | |
| 182 | + name: "Custom Microsoft Graph", | |
| 183 | + description: "Custom Microsoft Graph scopes.", | |
| 184 | + baseUrl: "", | |
| 185 | + }), | |
| 186 | + ).toEqual({ | |
| 187 | + presetIds: [], | |
| 188 | + customScopes: ["Sites.Read.All", "Custom.Scope"], | |
| 189 | + slug: "microsoft_graph_custom", | |
| 190 | + name: "Custom Microsoft Graph", | |
| 191 | + description: "Custom Microsoft Graph scopes.", | |
| 192 | + }); | |
| 193 | + }); | |
| 172 | 194 | }); |
| @@ -273,14 +273,25 @@ type MicrosoftGraphAddPayload = MicrosoftGraphConfig & { · 272 unmodified lines | ||
| 273 | 273 | readonly presetIds: readonly string[]; |
| 274 | 274 | }; |
| 275 | 275 | |
| 276 | −const microsoftGraphPayload = (config: MicrosoftGraphAddPayload): MicrosoftGraphAddPayload => { | |
| 277 | − const baseUrl = config.baseUrl?.trim() ?? ""; | |
| 278 | − const description = config.description?.trim() ?? ""; | |
| 276 | +// The payload for the legacy addGraph call that carries the custom scopes. | |
| 277 | +// The checked workloads already fan out as their own integrations through | |
| 278 | +// addWorkloads in the same submit, so the custom bundle must carry only the | |
| 279 | +// custom scopes (presetIds stays empty): folding the preset ids in again | |
| 280 | +// would create a second integration containing the same tools. | |
| 281 | +export const microsoftCustomGraphPayload = (input: { | |
| 282 | + readonly customScopes: readonly string[]; | |
| 283 | + readonly slug: string; | |
| 284 | + readonly name: string; | |
| 285 | + readonly description: string; | |
| 286 | + readonly baseUrl: string; | |
| 287 | +}): MicrosoftGraphAddPayload => { | |
| 288 | + const baseUrl = input.baseUrl.trim(); | |
| 289 | + const description = input.description.trim(); | |
| 279 | 290 | return { |
| 280 | − presetIds: config.presetIds, | |
| 281 | − ...(config.customScopes !== undefined ? { customScopes: config.customScopes } : {}), | |
| 282 | − ...(config.slug !== undefined ? { slug: config.slug } : {}), | |
| 283 | − ...(config.name !== undefined ? { name: config.name } : {}), | |
| 291 | + presetIds: [], | |
| 292 | + customScopes: [...input.customScopes], | |
| 293 | + slug: input.slug, | |
| 294 | + name: input.name, | |
| 284 | 295 | ...(description.length > 0 ? { description } : {}), |
| 285 | 296 | ...(baseUrl.length > 0 ? { baseUrl } : {}), |
| 286 | 297 | }; |
| @@ -366,9 +377,8 @@ export default function AddMicrosoftSource(props: { · 79 unmodified lines | ||
| 366 | 377 | const addCustomGraphScopes = async (): Promise<boolean> => { |
| 367 | 378 | if (customScopes.length === 0) return true; |
| 368 | 379 | const exit = await doAddGraph({ |
| 369 | − payload: microsoftGraphPayload({ | |
| 370 | − presetIds: selectedIds, | |
| 371 | − customScopes: [...customScopes], | |
| 380 | + payload: microsoftCustomGraphPayload({ | |
| 381 | + customScopes, | |
| 372 | 382 | slug: resolvedSourceId, |
| 373 | 383 | name: resolvedDisplayName, |
| 374 | 384 | description: resolvedDescription, |
| @@ -124,12 +124,17 @@ const uniqueStrings = (values: Iterable<string>): readonly string[] => { · 123 unmodified lines | ||
| 124 | 124 | }; |
| 125 | 125 | |
| 126 | 126 | const normalizeSelection = (input: MicrosoftGraphSelectionInput) => { |
| 127 | + const customScopes = uniqueStrings(input.customScopes ?? []); | |
| 128 | + // Explicitly empty presetIds alongside custom scopes means "custom scopes | |
| 129 | + // only": the add flow fans checked workloads out as their own integrations | |
| 130 | + // through addWorkloads, so folding the default presets back into the custom | |
| 131 | + // bundle would duplicate their tools. An empty selection without custom | |
| 132 | + // scopes still falls back to the default workloads. | |
| 127 | 133 | const presetIds = uniqueStrings( |
| 128 | − input.presetIds && input.presetIds.length > 0 | |
| 134 | + input.presetIds && (input.presetIds.length > 0 || customScopes.length > 0) | |
| 129 | 135 | ? input.presetIds |
| 130 | 136 | : MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, |
| 131 | 137 | ); |
| 132 | − const customScopes = uniqueStrings(input.customScopes ?? []); | |
| 133 | 138 | const scopes = microsoftGraphScopesForPresetIds(presetIds, customScopes); |
| 134 | 139 | const exactPaths = microsoftGraphExactPathsForPresetIds(presetIds); |
| 135 | 140 | const pathPrefixes = microsoftGraphPathPrefixesForPresetIds(presetIds); |
| @@ -407,6 +407,33 @@ describe("Microsoft Graph provider", () => { · 406 unmodified lines | ||
| 407 | 407 | ), |
| 408 | 408 | ); |
| 409 | 409 | |
| 410 | + it.effect("empty presetIds with custom scopes stays custom-only, no default workloads", () => | |
| 411 | + Effect.scoped( | |
| 412 | + Effect.gen(function* () { | |
| 413 | + const executor = yield* createExecutor(makeTestConfig({ plugins: graphPlugins() })); | |
| 414 | + | |
| 415 | + // The add flow fans checked workloads out through addWorkloads and | |
| 416 | + // sends the custom scopes here with presetIds: []. Folding the | |
| 417 | + // defaults back in would duplicate the fanned-out workloads' tools. | |
| 418 | + yield* executor.microsoft.addGraph({ | |
| 419 | + slug: "microsoft_graph_custom", | |
| 420 | + presetIds: [], | |
| 421 | + customScopes: ["Custom.Scope"], | |
| 422 | + }); | |
| 423 | + | |
| 424 | + const config = yield* executor.microsoft.getConfig("microsoft_graph_custom"); | |
| 425 | + expect(config?.microsoftGraphPresetIds).toEqual([]); | |
| 426 | + expect(config?.microsoftGraphCustomScopes).toEqual(["Custom.Scope"]); | |
| 427 | + expect(config?.microsoftGraphCoversFullGraph).toBe(false); | |
| 428 | + expect(config?.microsoftGraphScopes).toEqual([ | |
| 429 | + "offline_access", | |
| 430 | + "User.Read", | |
| 431 | + "Custom.Scope", | |
| 432 | + ]); | |
| 433 | + }), | |
| 434 | + ), | |
| 435 | + ); | |
| 436 | + | |
| 410 | 437 | it.effect("uses the app registration default scope when every Graph workload is selected", () => |
| 411 | 438 | Effect.scoped( |
| 412 | 439 | Effect.gen(function* () { |