@kya-os/mcp-i-core 1.3.0 → 1.3.2
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test$colon$coverage.log +2579 -2669
- package/.turbo/turbo-test.log +1251 -1245
- package/coverage/coverage-final.json +4 -4
- package/dist/services/oauth-config.service.d.ts.map +1 -1
- package/dist/services/oauth-config.service.js +5 -3
- package/dist/services/oauth-config.service.js.map +1 -1
- package/dist/services/oauth-provider-registry.d.ts +11 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -1
- package/dist/services/oauth-provider-registry.js +16 -0
- package/dist/services/oauth-provider-registry.js.map +1 -1
- package/dist/services/provider-resolver.d.ts +1 -1
- package/dist/services/provider-resolver.d.ts.map +1 -1
- package/dist/services/provider-resolver.js +14 -13
- package/dist/services/provider-resolver.js.map +1 -1
- package/dist/services/tool-protection.service.d.ts +13 -10
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +24 -113
- package/dist/services/tool-protection.service.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/regression/phase2-regression.test.ts +8 -6
- package/src/__tests__/services/cache-no-warming.test.ts +177 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +168 -64
- package/src/services/__tests__/provider-resolution.integration.test.ts +9 -5
- package/src/services/__tests__/provider-resolver.test.ts +22 -26
- package/src/services/oauth-config.service.ts +6 -3
- package/src/services/oauth-provider-registry.ts +18 -0
- package/src/services/provider-resolver.ts +15 -13
- package/src/services/tool-protection.service.ts +25 -136
- package/src/cache/oauth-config-cache.js +0 -71
- package/src/providers/base.js +0 -38
- package/src/services/oauth-config.service.js +0 -113
- package/src/services/oauth-provider-registry.js +0 -73
- package/src/services/provider-resolver.js +0 -106
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Invalidation: No Warming Test
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL TEST: Proves that clearAndRefresh does NOT fetch immediately after clearing.
|
|
5
|
+
*
|
|
6
|
+
* Background:
|
|
7
|
+
* - In mcp-i-cloudflare@1.6.4+, cache warming was added as an "optimization"
|
|
8
|
+
* - The idea was to fetch fresh data immediately after clearing
|
|
9
|
+
* - BUT this backfired when upstream APIs (AgentShield) have CDN caching
|
|
10
|
+
* - The immediate fetch would get stale CDN data and store it in our cache
|
|
11
|
+
* - This defeated the purpose of cache invalidation
|
|
12
|
+
*
|
|
13
|
+
* Fix:
|
|
14
|
+
* - clearAndRefresh now just DELETES the cache entry
|
|
15
|
+
* - It does NOT fetch or warm the cache
|
|
16
|
+
* - The next actual tool call will fetch fresh data (with natural delay)
|
|
17
|
+
*
|
|
18
|
+
* This was the behavior in mcp-i-cloudflare@1.6.3 that worked perfectly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
22
|
+
import { ToolProtectionService } from "../../services/tool-protection.service.js";
|
|
23
|
+
import { InMemoryToolProtectionCache } from "../../cache/tool-protection-cache.js";
|
|
24
|
+
|
|
25
|
+
describe("Cache Invalidation - No Warming", () => {
|
|
26
|
+
let cache: InMemoryToolProtectionCache;
|
|
27
|
+
let service: ToolProtectionService;
|
|
28
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
cache = new InMemoryToolProtectionCache();
|
|
32
|
+
fetchSpy = vi.fn();
|
|
33
|
+
|
|
34
|
+
service = new ToolProtectionService(
|
|
35
|
+
{
|
|
36
|
+
apiUrl: "https://test.agentshield.com",
|
|
37
|
+
apiKey: "test-key",
|
|
38
|
+
projectId: "test-project",
|
|
39
|
+
cacheTtl: 300000,
|
|
40
|
+
debug: true,
|
|
41
|
+
},
|
|
42
|
+
cache
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Spy on the internal fetch method
|
|
46
|
+
(service as any).fetchFromApi = fetchSpy;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("clearAndRefresh should DELETE cache without fetching", async () => {
|
|
50
|
+
// Seed the cache with old data
|
|
51
|
+
const oldConfig = {
|
|
52
|
+
toolProtections: {
|
|
53
|
+
greet: { requiresDelegation: false, requiredScopes: ["greet:execute"] },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
await cache.set("config:tool-protections:test-project", oldConfig, 300000);
|
|
57
|
+
|
|
58
|
+
// Clear the cache
|
|
59
|
+
const result = await service.clearAndRefresh("did:key:test-agent");
|
|
60
|
+
|
|
61
|
+
// CRITICAL: fetchFromApi should NOT have been called
|
|
62
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
63
|
+
|
|
64
|
+
// Result should indicate cache was cleared, not refreshed
|
|
65
|
+
expect(result.source).toBe("cleared");
|
|
66
|
+
expect(result.cacheKey).toBe("config:tool-protections:test-project");
|
|
67
|
+
expect(result.config.toolProtections).toEqual({});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("cache should be empty after clearAndRefresh (no warming)", async () => {
|
|
71
|
+
// Seed the cache
|
|
72
|
+
const oldConfig = {
|
|
73
|
+
toolProtections: {
|
|
74
|
+
greet: { requiresDelegation: true, requiredScopes: ["greet:execute"] },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
await cache.set("config:tool-protections:test-project", oldConfig, 300000);
|
|
78
|
+
|
|
79
|
+
// Verify it's cached
|
|
80
|
+
const beforeClear = await cache.get("config:tool-protections:test-project");
|
|
81
|
+
expect(beforeClear).not.toBeNull();
|
|
82
|
+
expect(beforeClear?.toolProtections.greet.requiresDelegation).toBe(true);
|
|
83
|
+
|
|
84
|
+
// Clear the cache
|
|
85
|
+
await service.clearAndRefresh("did:key:test-agent");
|
|
86
|
+
|
|
87
|
+
// Cache should be EMPTY (not warmed with new data)
|
|
88
|
+
const afterClear = await cache.get("config:tool-protections:test-project");
|
|
89
|
+
expect(afterClear).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("next getToolProtectionConfig call should fetch fresh from API", async () => {
|
|
93
|
+
// Setup: Mock API to return DIFFERENT data than what was cached
|
|
94
|
+
const staleConfig = {
|
|
95
|
+
toolProtections: {
|
|
96
|
+
greet: { requiresDelegation: false, requiredScopes: [] },
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Seed cache with stale data
|
|
101
|
+
await cache.set(
|
|
102
|
+
"config:tool-protections:test-project",
|
|
103
|
+
staleConfig,
|
|
104
|
+
300000
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Mock API to return fresh data (requiresDelegation: TRUE)
|
|
108
|
+
fetchSpy.mockResolvedValueOnce({
|
|
109
|
+
data: {
|
|
110
|
+
toolProtections: {
|
|
111
|
+
greet: {
|
|
112
|
+
requiresDelegation: true,
|
|
113
|
+
requiredScopes: ["greet:execute"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Clear cache (should NOT fetch)
|
|
120
|
+
await service.clearAndRefresh("did:key:test-agent");
|
|
121
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
122
|
+
|
|
123
|
+
// Now get config - this SHOULD fetch from API
|
|
124
|
+
const result = await service.getToolProtectionConfig("did:key:test-agent");
|
|
125
|
+
|
|
126
|
+
// API should have been called now (cache was cleared, so it fetched)
|
|
127
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
128
|
+
|
|
129
|
+
// Result should have fresh data from API (not stale cached data)
|
|
130
|
+
expect(result.toolProtections.greet.requiresDelegation).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("proves stale CDN data issue is avoided", async () => {
|
|
134
|
+
/**
|
|
135
|
+
* This test proves why NO warming is better than warming.
|
|
136
|
+
*
|
|
137
|
+
* Scenario:
|
|
138
|
+
* 1. User toggles delegation ON in dashboard
|
|
139
|
+
* 2. AgentShield DB updates, but CDN still has old data (5 min TTL)
|
|
140
|
+
* 3. User triggers cache clear
|
|
141
|
+
*
|
|
142
|
+
* WITH warming (broken):
|
|
143
|
+
* - clearAndRefresh immediately fetches from API
|
|
144
|
+
* - API returns stale CDN data (requiresDelegation: false)
|
|
145
|
+
* - We store stale data in our cache
|
|
146
|
+
* - Tool call uses stale data = BUG
|
|
147
|
+
*
|
|
148
|
+
* WITHOUT warming (fixed):
|
|
149
|
+
* - clearAndRefresh just deletes cache
|
|
150
|
+
* - Next tool call fetches from API (some time later)
|
|
151
|
+
* - Better chance CDN has refreshed
|
|
152
|
+
* - Even if CDN still stale, user can retry after CDN TTL
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
// Simulate stale CDN response (old config before user toggled delegation)
|
|
156
|
+
const staleCdnResponse = {
|
|
157
|
+
data: {
|
|
158
|
+
toolProtections: {
|
|
159
|
+
greet: { requiresDelegation: false, requiredScopes: [] },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
fetchSpy.mockResolvedValue(staleCdnResponse);
|
|
165
|
+
|
|
166
|
+
// Clear cache
|
|
167
|
+
await service.clearAndRefresh("did:key:test-agent");
|
|
168
|
+
|
|
169
|
+
// CRITICAL: We did NOT store the stale CDN data
|
|
170
|
+
// fetchSpy was NOT called during clearAndRefresh
|
|
171
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
172
|
+
|
|
173
|
+
// Cache is empty, not poisoned with stale data
|
|
174
|
+
const cached = await cache.get("config:tool-protections:test-project");
|
|
175
|
+
expect(cached).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -57,12 +57,14 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
57
57
|
const getProviderNamesMock = vi.fn().mockReturnValue([]);
|
|
58
58
|
const loadFromAgentShieldMock = vi.fn().mockResolvedValue(undefined);
|
|
59
59
|
const hasProviderMock = vi.fn().mockReturnValue(false);
|
|
60
|
+
const getConfiguredProviderMock = vi.fn().mockReturnValue(null);
|
|
60
61
|
|
|
61
62
|
mockRegistry = {
|
|
62
63
|
hasProvider: hasProviderMock,
|
|
63
64
|
getAllProviders: vi.fn().mockReturnValue([]),
|
|
64
65
|
getProviderNames: getProviderNamesMock,
|
|
65
66
|
loadFromAgentShield: loadFromAgentShieldMock,
|
|
67
|
+
getConfiguredProvider: getConfiguredProviderMock,
|
|
66
68
|
} as any;
|
|
67
69
|
|
|
68
70
|
resolver = new ProviderResolver(mockRegistry, mockConfigService);
|
|
@@ -154,19 +156,16 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
154
156
|
requiredScopes: [],
|
|
155
157
|
};
|
|
156
158
|
|
|
157
|
-
// Should fall through to Priority 3
|
|
158
|
-
(mockRegistry.hasProvider as any).
|
|
159
|
-
(mockRegistry.
|
|
160
|
-
{ clientId: "github_client_id" },
|
|
161
|
-
]);
|
|
162
|
-
(mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
|
|
159
|
+
// Should fall through to Priority 3 (configuredProvider)
|
|
160
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
161
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
163
162
|
|
|
164
163
|
const provider = await resolver.resolveProvider(
|
|
165
164
|
toolProtection,
|
|
166
165
|
"test-project"
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
// Should use
|
|
168
|
+
// Should use configured provider (Priority 3)
|
|
170
169
|
expect(provider).toBe("github");
|
|
171
170
|
});
|
|
172
171
|
|
|
@@ -176,19 +175,16 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
176
175
|
requiredScopes: ["github:repo:read", "google:calendar:read"],
|
|
177
176
|
};
|
|
178
177
|
|
|
179
|
-
// Ambiguous scopes should fall through to Priority 3
|
|
180
|
-
(mockRegistry.hasProvider as any).
|
|
181
|
-
(mockRegistry.
|
|
182
|
-
{ clientId: "github_client_id" },
|
|
183
|
-
]);
|
|
184
|
-
(mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
|
|
178
|
+
// Ambiguous scopes should fall through to Priority 3 (configuredProvider)
|
|
179
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
180
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
185
181
|
|
|
186
182
|
const provider = await resolver.resolveProvider(
|
|
187
183
|
toolProtection,
|
|
188
184
|
"test-project"
|
|
189
185
|
);
|
|
190
186
|
|
|
191
|
-
// Should use
|
|
187
|
+
// Should use configured provider (Priority 3)
|
|
192
188
|
expect(provider).toBe("github");
|
|
193
189
|
});
|
|
194
190
|
|
|
@@ -198,19 +194,16 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
198
194
|
requiredScopes: ["custom:unknown:scope"],
|
|
199
195
|
};
|
|
200
196
|
|
|
201
|
-
// Unknown prefix should fall through to Priority 3
|
|
202
|
-
(mockRegistry.hasProvider as any).
|
|
203
|
-
(mockRegistry.
|
|
204
|
-
{ clientId: "github_client_id" },
|
|
205
|
-
]);
|
|
206
|
-
(mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
|
|
197
|
+
// Unknown prefix should fall through to Priority 3 (configuredProvider)
|
|
198
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
199
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
207
200
|
|
|
208
201
|
const provider = await resolver.resolveProvider(
|
|
209
202
|
toolProtection,
|
|
210
203
|
"test-project"
|
|
211
204
|
);
|
|
212
205
|
|
|
213
|
-
// Should use
|
|
206
|
+
// Should use configured provider (Priority 3)
|
|
214
207
|
expect(provider).toBe("github");
|
|
215
208
|
});
|
|
216
209
|
|
|
@@ -277,19 +270,16 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
277
270
|
requiredScopes: ["read", "write"], // No colons
|
|
278
271
|
};
|
|
279
272
|
|
|
280
|
-
// Should fall through to Priority 3
|
|
281
|
-
(mockRegistry.hasProvider as any).
|
|
282
|
-
(mockRegistry.
|
|
283
|
-
{ clientId: "github_client_id" },
|
|
284
|
-
]);
|
|
285
|
-
(mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
|
|
273
|
+
// Should fall through to Priority 3 (configuredProvider)
|
|
274
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
275
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
286
276
|
|
|
287
277
|
const provider = await resolver.resolveProvider(
|
|
288
278
|
toolProtection,
|
|
289
279
|
"test-project"
|
|
290
280
|
);
|
|
291
281
|
|
|
292
|
-
// Should use
|
|
282
|
+
// Should use configured provider (Priority 3)
|
|
293
283
|
expect(provider).toBe("github");
|
|
294
284
|
});
|
|
295
285
|
|
|
@@ -299,90 +289,102 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
299
289
|
requiredScopes: ["github:repo:read"],
|
|
300
290
|
};
|
|
301
291
|
|
|
302
|
-
// Inferred provider exists but not in registry
|
|
303
|
-
|
|
304
|
-
(mockRegistry.hasProvider as any).mockReturnValue(false); // github not in registry
|
|
305
|
-
|
|
306
|
-
// Should fall through to Priority 3
|
|
307
|
-
(mockRegistry.getAllProviders as any).mockReturnValue([
|
|
308
|
-
{ clientId: "google_client_id" },
|
|
309
|
-
]);
|
|
292
|
+
// Inferred provider (github) exists but not in registry
|
|
293
|
+
// configuredProvider is google
|
|
310
294
|
(mockRegistry.getProviderNames as any).mockReturnValue(["google"]);
|
|
295
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "google");
|
|
296
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("google");
|
|
311
297
|
|
|
312
298
|
const provider = await resolver.resolveProvider(
|
|
313
299
|
toolProtection,
|
|
314
300
|
"test-project"
|
|
315
301
|
);
|
|
316
302
|
|
|
317
|
-
// Should use
|
|
303
|
+
// Should use configured provider (Priority 3)
|
|
318
304
|
expect(provider).toBe("google");
|
|
319
305
|
});
|
|
320
306
|
});
|
|
321
307
|
|
|
322
|
-
describe("Fallback behavior (Priority 3)", () => {
|
|
323
|
-
it("should log
|
|
308
|
+
describe("Fallback behavior (Priority 3 - configuredProvider)", () => {
|
|
309
|
+
it("should log warning when using configuredProvider fallback", async () => {
|
|
324
310
|
const toolProtection: ToolProtection = {
|
|
325
311
|
requiresDelegation: true,
|
|
326
312
|
requiredScopes: ["custom:scope"],
|
|
327
313
|
};
|
|
328
314
|
|
|
329
|
-
(mockRegistry.hasProvider as any).
|
|
330
|
-
(mockRegistry.
|
|
331
|
-
{ clientId: "github_client_id" },
|
|
332
|
-
]);
|
|
333
|
-
(mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
|
|
315
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
316
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
334
317
|
|
|
335
318
|
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
336
319
|
|
|
337
320
|
await resolver.resolveProvider(toolProtection, "test-project");
|
|
338
321
|
|
|
339
322
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
340
|
-
expect.stringContaining("
|
|
323
|
+
expect.stringContaining("project-configured provider")
|
|
341
324
|
);
|
|
342
325
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
343
|
-
expect.stringContaining("
|
|
326
|
+
expect.stringContaining("github")
|
|
344
327
|
);
|
|
345
328
|
|
|
346
329
|
consoleSpy.mockRestore();
|
|
347
330
|
});
|
|
348
331
|
|
|
349
|
-
it("should use
|
|
332
|
+
it("should use configuredProvider when oauthProvider not specified", async () => {
|
|
350
333
|
const toolProtection: ToolProtection = {
|
|
351
334
|
requiresDelegation: true,
|
|
352
335
|
requiredScopes: ["custom:scope"],
|
|
353
336
|
};
|
|
354
337
|
|
|
338
|
+
// Multiple providers available, but configuredProvider is google
|
|
339
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) =>
|
|
340
|
+
name === "github" || name === "google"
|
|
341
|
+
);
|
|
342
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("google");
|
|
343
|
+
|
|
344
|
+
const provider = await resolver.resolveProvider(
|
|
345
|
+
toolProtection,
|
|
346
|
+
"test-project"
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Should use configuredProvider, not first alphabetically
|
|
350
|
+
expect(provider).toBe("google");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should NOT fall back to first provider alphabetically", async () => {
|
|
354
|
+
const toolProtection: ToolProtection = {
|
|
355
|
+
requiresDelegation: true,
|
|
356
|
+
requiredScopes: ["custom:scope"],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Multiple providers in registry, but NO configuredProvider
|
|
355
360
|
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
361
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
356
362
|
(mockRegistry.getAllProviders as any).mockReturnValue([
|
|
357
363
|
{ clientId: "github_client_id" },
|
|
358
364
|
{ clientId: "google_client_id" },
|
|
359
365
|
]);
|
|
360
366
|
(mockRegistry.getProviderNames as any).mockReturnValue(["github", "google"]);
|
|
361
367
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
"test-project"
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
// Should use first provider
|
|
368
|
-
expect(provider).toBe("github");
|
|
368
|
+
// Should throw error, NOT pick first provider
|
|
369
|
+
await expect(
|
|
370
|
+
resolver.resolveProvider(toolProtection, "test-project")
|
|
371
|
+
).rejects.toThrow(/no provider is configured/);
|
|
369
372
|
});
|
|
370
373
|
});
|
|
371
374
|
|
|
372
375
|
describe("Error scenarios (Priority 4)", () => {
|
|
373
|
-
it("should throw error if no
|
|
376
|
+
it("should throw error if no provider is configured", async () => {
|
|
374
377
|
const toolProtection: ToolProtection = {
|
|
375
378
|
requiresDelegation: true,
|
|
376
379
|
requiredScopes: [],
|
|
377
380
|
};
|
|
378
381
|
|
|
379
382
|
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
380
|
-
(mockRegistry.
|
|
381
|
-
(mockRegistry.getProviderNames as any).mockReturnValue([]);
|
|
383
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
382
384
|
|
|
383
385
|
await expect(
|
|
384
386
|
resolver.resolveProvider(toolProtection, "test-project")
|
|
385
|
-
).rejects.toThrow(/no provider
|
|
387
|
+
).rejects.toThrow(/no provider is configured/);
|
|
386
388
|
});
|
|
387
389
|
|
|
388
390
|
it("should include project ID in error message", async () => {
|
|
@@ -392,8 +394,7 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
392
394
|
};
|
|
393
395
|
|
|
394
396
|
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
395
|
-
(mockRegistry.
|
|
396
|
-
(mockRegistry.getProviderNames as any).mockReturnValue([]);
|
|
397
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
397
398
|
|
|
398
399
|
try {
|
|
399
400
|
await resolver.resolveProvider(toolProtection, "test-project-123");
|
|
@@ -409,15 +410,14 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
409
410
|
};
|
|
410
411
|
|
|
411
412
|
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
412
|
-
(mockRegistry.
|
|
413
|
-
(mockRegistry.getProviderNames as any).mockReturnValue([]);
|
|
413
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
414
414
|
|
|
415
415
|
try {
|
|
416
416
|
await resolver.resolveProvider(toolProtection, "test-project");
|
|
417
417
|
} catch (error) {
|
|
418
418
|
const message = (error as Error).message;
|
|
419
|
-
expect(message).toContain("
|
|
420
|
-
expect(message).toContain("
|
|
419
|
+
expect(message).toContain("AgentShield dashboard");
|
|
420
|
+
expect(message).toContain("Configure");
|
|
421
421
|
}
|
|
422
422
|
});
|
|
423
423
|
});
|
|
@@ -483,5 +483,109 @@ describe("ProviderResolver - Edge Cases", () => {
|
|
|
483
483
|
expect(provider).toBe("github");
|
|
484
484
|
});
|
|
485
485
|
});
|
|
486
|
+
|
|
487
|
+
describe("configuredProvider behavior (Priority 3)", () => {
|
|
488
|
+
it("should use configuredProvider when tool has no oauthProvider", async () => {
|
|
489
|
+
const toolProtection: ToolProtection = {
|
|
490
|
+
requiresDelegation: true,
|
|
491
|
+
requiredScopes: ["greet:execute"], // No scope inference match
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// configuredProvider is github
|
|
495
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
|
|
496
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
497
|
+
|
|
498
|
+
const provider = await resolver.resolveProvider(
|
|
499
|
+
toolProtection,
|
|
500
|
+
"test-project"
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(provider).toBe("github");
|
|
504
|
+
expect(mockRegistry.getConfiguredProvider).toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should throw when configuredProvider is null and tool needs OAuth", async () => {
|
|
508
|
+
const toolProtection: ToolProtection = {
|
|
509
|
+
requiresDelegation: true,
|
|
510
|
+
requiredScopes: ["greet:execute"],
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// No configuredProvider set
|
|
514
|
+
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
515
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
516
|
+
|
|
517
|
+
await expect(
|
|
518
|
+
resolver.resolveProvider(toolProtection, "test-project")
|
|
519
|
+
).rejects.toThrow(/no provider is configured/);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("should NOT use unconfigured providers as fallback", async () => {
|
|
523
|
+
const toolProtection: ToolProtection = {
|
|
524
|
+
requiresDelegation: true,
|
|
525
|
+
requiredScopes: ["greet:execute"],
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Registry has providers but configuredProvider is null
|
|
529
|
+
// This simulates AgentShield returning all providers but none configured
|
|
530
|
+
(mockRegistry.hasProvider as any).mockReturnValue(false);
|
|
531
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
|
|
532
|
+
(mockRegistry.getAllProviders as any).mockReturnValue([
|
|
533
|
+
{ clientId: "github_client_id" },
|
|
534
|
+
{ clientId: "google_client_id" },
|
|
535
|
+
{ clientId: "microsoft_client_id" },
|
|
536
|
+
]);
|
|
537
|
+
(mockRegistry.getProviderNames as any).mockReturnValue(["github", "google", "microsoft"]);
|
|
538
|
+
|
|
539
|
+
// Should NOT pick "github" alphabetically - should throw
|
|
540
|
+
await expect(
|
|
541
|
+
resolver.resolveProvider(toolProtection, "test-project")
|
|
542
|
+
).rejects.toThrow(/no provider is configured/);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("should prefer tool-specific oauthProvider over configuredProvider", async () => {
|
|
546
|
+
const toolProtection: ToolProtection = {
|
|
547
|
+
requiresDelegation: true,
|
|
548
|
+
requiredScopes: [],
|
|
549
|
+
oauthProvider: "google", // Tool specifies google
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// configuredProvider is github, but tool wants google
|
|
553
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) =>
|
|
554
|
+
name === "github" || name === "google"
|
|
555
|
+
);
|
|
556
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
557
|
+
(mockRegistry.getProviderNames as any).mockReturnValue(["github", "google"]);
|
|
558
|
+
|
|
559
|
+
const provider = await resolver.resolveProvider(
|
|
560
|
+
toolProtection,
|
|
561
|
+
"test-project"
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Priority 1 (tool-specific) should win over Priority 3 (configuredProvider)
|
|
565
|
+
expect(provider).toBe("google");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("should prefer scope-inferred provider over configuredProvider", async () => {
|
|
569
|
+
const toolProtection: ToolProtection = {
|
|
570
|
+
requiresDelegation: true,
|
|
571
|
+
requiredScopes: ["google:calendar:read"], // Infers google
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// configuredProvider is github, but scopes infer google
|
|
575
|
+
(mockRegistry.hasProvider as any).mockImplementation((name: string) =>
|
|
576
|
+
name === "github" || name === "google"
|
|
577
|
+
);
|
|
578
|
+
(mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
|
|
579
|
+
(mockRegistry.getProviderNames as any).mockReturnValue(["github", "google"]);
|
|
580
|
+
|
|
581
|
+
const provider = await resolver.resolveProvider(
|
|
582
|
+
toolProtection,
|
|
583
|
+
"test-project"
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// Priority 2 (scope inference) should win over Priority 3 (configuredProvider)
|
|
587
|
+
expect(provider).toBe("google");
|
|
588
|
+
});
|
|
589
|
+
});
|
|
486
590
|
});
|
|
487
591
|
|
|
@@ -37,6 +37,8 @@ describe("Provider Resolution Integration", () => {
|
|
|
37
37
|
requiresClientSecret: false,
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
|
+
// Explicitly configured provider (Priority 3 fallback)
|
|
41
|
+
configuredProvider: "github",
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
beforeEach(() => {
|
|
@@ -95,7 +97,7 @@ describe("Provider Resolution Integration", () => {
|
|
|
95
97
|
consoleSpy.mockRestore();
|
|
96
98
|
});
|
|
97
99
|
|
|
98
|
-
it("should fall back to
|
|
100
|
+
it("should fall back to configuredProvider for Phase 1 compatibility", async () => {
|
|
99
101
|
await providerRegistry.loadFromAgentShield("test-project");
|
|
100
102
|
|
|
101
103
|
const toolProtection: ToolProtection = {
|
|
@@ -111,10 +113,10 @@ describe("Provider Resolution Integration", () => {
|
|
|
111
113
|
"test-project"
|
|
112
114
|
);
|
|
113
115
|
|
|
114
|
-
// Should use
|
|
116
|
+
// Should use configuredProvider (github)
|
|
115
117
|
expect(provider).toBe("github");
|
|
116
118
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
117
|
-
expect.stringContaining("
|
|
119
|
+
expect.stringContaining("project-configured provider")
|
|
118
120
|
);
|
|
119
121
|
|
|
120
122
|
consoleSpy.mockRestore();
|
|
@@ -157,9 +159,11 @@ describe("Provider Resolution Integration", () => {
|
|
|
157
159
|
"test-project"
|
|
158
160
|
);
|
|
159
161
|
|
|
160
|
-
// Should fall back to
|
|
162
|
+
// Should fall back to configuredProvider
|
|
161
163
|
expect(provider).toBe("github");
|
|
162
|
-
expect(consoleSpy).
|
|
164
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
165
|
+
expect.stringContaining("project-configured provider")
|
|
166
|
+
);
|
|
163
167
|
|
|
164
168
|
consoleSpy.mockRestore();
|
|
165
169
|
});
|