// Amazon Bedrock Mantle tests cover discovery plugin behavior. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { discoverMantleModels, generateBearerTokenFromIam, getCachedIamToken, MANTLE_IAM_TOKEN_MARKER, mergeImplicitMantleProvider, resetIamTokenCacheForTest, resetMantleDiscoveryCacheForTest, resolveImplicitMantleProvider, resolveMantleBearerToken, resolveMantleRuntimeBearerToken, } = await import("./api.js"); function createTokenProviderFactory(tokenProvider: () => Promise) { return vi.fn(() => tokenProvider); } type MockWithCalls = { mock: { calls: unknown[][] }; }; function argAt(mock: MockWithCalls, callIndex: number, argIndex: number): unknown { const call = mock.mock.calls[callIndex]; if (!call) { throw new Error(`expected call ${callIndex}`); } if (!(argIndex in call)) { throw new Error(`expected call ${callIndex} argument ${argIndex}`); } return call[argIndex]; } function objectArgAt( mock: MockWithCalls, callIndex: number, argIndex: number, ): Record { const value = argAt(mock, callIndex, argIndex); if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) { throw new Error(`expected call ${callIndex} argument ${argIndex} to be an object`); } return value as Record; } function stringArgAt(mock: MockWithCalls, callIndex: number, argIndex: number): string { const value = argAt(mock, callIndex, argIndex); if (typeof value !== "string") { throw new Error(`expected call ${callIndex} argument ${argIndex} to be a string`); } return value; } function recordField(value: unknown, field: string): Record { if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) { throw new Error(`expected ${field} to be an object`); } return value as Record; } describe("bedrock mantle discovery", () => { const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; vi.restoreAllMocks(); resetMantleDiscoveryCacheForTest(); resetIamTokenCacheForTest(); }); afterEach(() => { vi.restoreAllMocks(); resetMantleDiscoveryCacheForTest(); resetIamTokenCacheForTest(); process.env = originalEnv; }); // --------------------------------------------------------------------------- // Bearer token resolution // --------------------------------------------------------------------------- it("resolves bearer token from AWS_BEARER_TOKEN_BEDROCK", () => { expect( resolveMantleBearerToken({ AWS_BEARER_TOKEN_BEDROCK: "bedrock-api-key-abc123", // pragma: allowlist secret } as NodeJS.ProcessEnv), ).toBe("bedrock-api-key-abc123"); }); it("returns undefined when no bearer token env var is set", () => { expect(resolveMantleBearerToken({} as NodeJS.ProcessEnv)).toBeUndefined(); }); it("trims whitespace from bearer token", () => { expect( resolveMantleBearerToken({ AWS_BEARER_TOKEN_BEDROCK: " my-token ", // pragma: allowlist secret } as NodeJS.ProcessEnv), ).toBe("my-token"); }); // --------------------------------------------------------------------------- // IAM token generation // --------------------------------------------------------------------------- it("generates token from IAM credentials when token generation succeeds", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-generated"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); const token = await generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory, }); expect(token).toBe("bedrock-api-key-generated"); expect(tokenProviderFactory).toHaveBeenCalledWith({ region: "us-east-1", expiresInSeconds: 7200, }); expect(tokenProvider).toHaveBeenCalledTimes(1); }); it("caches generated IAM tokens within TTL", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-cached"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); let now = 1000; const t1 = await generateBearerTokenFromIam({ region: "us-east-1", now: () => now, tokenProviderFactory, }); now += 1800_000; // 30 min — within 2hr cache TTL const t2 = await generateBearerTokenFromIam({ region: "us-east-1", now: () => now, tokenProviderFactory, }); expect(t1).toEqual(t2); expect(tokenProvider).toHaveBeenCalledTimes(1); }); it("does not reuse an IAM token across regions", async () => { const tokenProvider = vi .fn<() => Promise>() .mockResolvedValueOnce("bedrock-api-key-east") // pragma: allowlist secret .mockResolvedValueOnce("bedrock-api-key-west"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); const east = await generateBearerTokenFromIam({ region: "us-east-1", now: () => 1000, tokenProviderFactory, }); const west = await generateBearerTokenFromIam({ region: "us-west-2", now: () => 2000, tokenProviderFactory, }); expect(east).toBe("bedrock-api-key-east"); expect(west).toBe("bedrock-api-key-west"); expect(tokenProviderFactory).toHaveBeenNthCalledWith(1, { region: "us-east-1", expiresInSeconds: 7200, }); expect(tokenProviderFactory).toHaveBeenNthCalledWith(2, { region: "us-west-2", expiresInSeconds: 7200, }); expect(tokenProvider).toHaveBeenCalledTimes(2); }); it("returns undefined when IAM token generation fails", async () => { const tokenProviderFactory = vi.fn(() => { throw new Error("no credentials"); }); await expect( generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory }), ).resolves.toBeUndefined(); }); it("skips IAM token generation when plugin discovery is disabled", async () => { const tokenProviderFactory = vi.fn(() => { throw new Error("disabled discovery should not generate a token"); }); await expect( resolveImplicitMantleProvider({ env: { AWS_REGION: "us-east-1" } as NodeJS.ProcessEnv, pluginConfig: { discovery: { enabled: false } }, tokenProviderFactory, }), ).resolves.toBeNull(); expect(tokenProviderFactory).not.toHaveBeenCalled(); }); it("getCachedIamToken returns cached token when valid", async () => { const tokenProvider = vi.fn(async () => "bedrock-cached-token"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); // Generate a token to populate the cache await generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory }); // Sync read should return the cached token expect(getCachedIamToken("us-east-1")).toBe("bedrock-cached-token"); }); it("getCachedIamToken returns undefined when cache is empty", () => { expect(getCachedIamToken("us-east-1")).toBeUndefined(); }); it("getCachedIamToken returns undefined when cache is expired", async () => { const tokenProvider = vi.fn(async () => "bedrock-expired-token"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); // Generate with a time far in the past so it's already expired await generateBearerTokenFromIam({ region: "us-east-1", now: () => 1000, tokenProviderFactory, }); // The cache entry exists but expiresAt is 1000 + 3600000 = 3601000 // Current Date.now() is way past that, so it should be expired expect(getCachedIamToken("us-east-1")).toBeUndefined(); }); it("does not cache generated IAM tokens when ttl expiry overflows", async () => { const tokenProvider = vi .fn<() => Promise>() .mockResolvedValueOnce("bedrock-overflow-token-1") // pragma: allowlist secret .mockResolvedValueOnce("bedrock-overflow-token-2"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); await expect( generateBearerTokenFromIam({ region: "us-east-1", now: () => 8_640_000_000_000_000, tokenProviderFactory, }), ).resolves.toBe("bedrock-overflow-token-1"); expect(getCachedIamToken("us-east-1")).toBeUndefined(); await expect( generateBearerTokenFromIam({ region: "us-east-1", now: () => 8_640_000_000_000_000, tokenProviderFactory, }), ).resolves.toBe("bedrock-overflow-token-2"); expect(tokenProvider).toHaveBeenCalledTimes(2); }); // --------------------------------------------------------------------------- // Model discovery // --------------------------------------------------------------------------- it("discovers models from Mantle /v1/models endpoint sorted by id", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [ { id: "openai.gpt-oss-120b", object: "model", owned_by: "openai" }, { id: "anthropic.claude-sonnet-4-6", object: "model", owned_by: "anthropic" }, { id: "mistral.devstral-2-123b", object: "model", owned_by: "mistral" }, ], }), }); const models = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, }); expect(models).toHaveLength(3); // Models should be sorted alphabetically by id expect(models[0]?.id).toBe("anthropic.claude-sonnet-4-6"); expect(models[0]?.name).toBe("anthropic.claude-sonnet-4-6"); expect(models[0]?.reasoning).toBe(false); expect(models[0]?.input).toEqual(["text"]); expect(models[1]?.id).toBe("mistral.devstral-2-123b"); expect(models[1]?.reasoning).toBe(false); expect(models[2]?.id).toBe("openai.gpt-oss-120b"); expect(models[2]?.reasoning).toBe(true); // GPT-OSS 120B supports reasoning // Verify correct endpoint and auth header expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models"); expect(recordField(objectArgAt(mockFetch, 0, 1).headers, "headers").Authorization).toBe( "Bearer test-token", ); }); it("infers reasoning support from model IDs", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [ { id: "moonshotai.kimi-k2-thinking", object: "model" }, { id: "openai.gpt-oss-120b", object: "model" }, { id: "openai.gpt-oss-safeguard-120b", object: "model" }, { id: "deepseek.v3.2", object: "model" }, { id: "mistral.mistral-large-3-675b-instruct", object: "model" }, ], }), }); const models = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, }); const byId = Object.fromEntries(models.map((m) => [m.id, m])); expect(byId["moonshotai.kimi-k2-thinking"]?.reasoning).toBe(true); expect(byId["openai.gpt-oss-120b"]?.reasoning).toBe(true); expect(byId["openai.gpt-oss-safeguard-120b"]?.reasoning).toBe(true); expect(byId["deepseek.v3.2"]?.reasoning).toBe(false); expect(byId["mistral.mistral-large-3-675b-instruct"]?.reasoning).toBe(false); }); it("returns empty array on permission error", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: false, status: 403, statusText: "Forbidden", }); const models = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, }); expect(models).toStrictEqual([]); }); it("returns empty array on network error", async () => { const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); const models = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, }); expect(models).toStrictEqual([]); }); it("filters out models with empty IDs", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [ { id: "anthropic.claude-sonnet-4-6", object: "model" }, { id: "", object: "model" }, { id: " ", object: "model" }, ], }), }); const models = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, }); expect(models).toHaveLength(1); expect(models[0]?.id).toBe("anthropic.claude-sonnet-4-6"); }); // --------------------------------------------------------------------------- // Discovery caching // --------------------------------------------------------------------------- it("returns cached models on subsequent calls within refresh interval", async () => { let now = 1000000; const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }], }), }); // First call — hits the network const first = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, now: () => now, }); expect(first).toHaveLength(1); expect(mockFetch).toHaveBeenCalledTimes(1); // Second call within refresh interval — uses cache now += 60_000; // 1 minute later const second = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, now: () => now, }); expect(second).toHaveLength(1); expect(mockFetch).toHaveBeenCalledTimes(1); // No additional fetch // Third call after refresh interval — re-fetches now += 3600_000; // 1 hour later const third = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, now: () => now, }); expect(third).toHaveLength(1); expect(mockFetch).toHaveBeenCalledTimes(2); // Re-fetched }); it("returns stale cache on fetch failure", async () => { let now = 1000000; const mockFetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }], }), }) .mockRejectedValueOnce(new Error("ECONNREFUSED")); // First call — succeeds await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, now: () => now, }); // Second call after expiry — fails but returns stale cache now += 7200_000; const stale = await discoverMantleModels({ region: "us-east-1", bearerToken: "test-token", fetchFn: mockFetch as unknown as typeof fetch, now: () => now, }); expect(stale).toHaveLength(1); expect(stale[0]?.id).toBe("anthropic.claude-sonnet-4-6"); }); // --------------------------------------------------------------------------- // Implicit provider resolution // --------------------------------------------------------------------------- it("resolves implicit provider when bearer token is set", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }], }), }); const provider = await resolveImplicitMantleProvider({ env: { AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret AWS_REGION: "us-east-1", } as NodeJS.ProcessEnv, fetchFn: mockFetch as unknown as typeof fetch, }); expect(provider?.baseUrl).toBe("https://bedrock-mantle.us-east-1.api.aws/v1"); expect(provider?.api).toBe("openai-completions"); expect(provider?.auth).toBe("api-key"); expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK"); expect(provider?.models).toHaveLength(3); const opus = provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7"); expect(opus?.api).toBe("anthropic-messages"); expect(opus?.reasoning).toBe(false); expect(opus).not.toHaveProperty("baseUrl"); const mythos = provider?.models?.find( (model) => model.id === "anthropic.claude-mythos-preview", ); expect(mythos).toMatchObject({ api: "anthropic-messages", reasoning: true, params: { canonicalModelId: "claude-mythos-preview" }, contextWindow: 1_000_000, maxTokens: 128_000, }); }); it("returns null when no auth is available", async () => { const tokenProviderFactory = vi.fn(() => { throw new Error("no credentials"); }); const provider = await resolveImplicitMantleProvider({ env: {} as NodeJS.ProcessEnv, tokenProviderFactory, }); expect(provider).toBeNull(); }); it("uses a generated IAM token when no explicit token is set", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-iam"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: "openai.gpt-oss-120b", object: "model" }], }), }); const provider = await resolveImplicitMantleProvider({ env: { AWS_PROFILE: "default", AWS_REGION: "us-east-1", } as NodeJS.ProcessEnv, fetchFn: mockFetch as unknown as typeof fetch, tokenProviderFactory, }); expect(provider?.apiKey).toBe(MANTLE_IAM_TOKEN_MARKER); expect(tokenProvider).toHaveBeenCalledTimes(1); expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models"); expect(recordField(objectArgAt(mockFetch, 0, 1).headers, "headers").Authorization).toBe( "Bearer bedrock-api-key-iam", ); }); it("resolves Mantle runtime auth from the cached IAM token marker", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-runtime"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); await generateBearerTokenFromIam({ region: "us-east-1", now: () => 1000, tokenProviderFactory, }); const resolved = await resolveMantleRuntimeBearerToken({ apiKey: MANTLE_IAM_TOKEN_MARKER, env: { AWS_REGION: "us-east-1", } as NodeJS.ProcessEnv, now: () => 2000, tokenProviderFactory, }); expect(resolved?.apiKey).toBe("bedrock-api-key-runtime"); expect(resolved?.expiresAt).toBe(1000 + 7200_000); expect(tokenProvider).toHaveBeenCalledTimes(1); }); it("generates a fresh Mantle runtime IAM token when the cache is cold", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-fresh"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); const resolved = await resolveMantleRuntimeBearerToken({ apiKey: MANTLE_IAM_TOKEN_MARKER, env: { AWS_REGION: "us-east-1", } as NodeJS.ProcessEnv, now: () => 5000, tokenProviderFactory, }); expect(resolved?.apiKey).toBe("bedrock-api-key-fresh"); expect(resolved?.expiresAt).toBe(5000 + 7200_000); expect(tokenProvider).toHaveBeenCalledTimes(1); }); it("omits Mantle runtime IAM token expiry when the process clock is invalid", async () => { const tokenProvider = vi.fn(async () => "bedrock-api-key-invalid-clock"); // pragma: allowlist secret const tokenProviderFactory = createTokenProviderFactory(tokenProvider); const resolved = await resolveMantleRuntimeBearerToken({ apiKey: MANTLE_IAM_TOKEN_MARKER, env: { AWS_REGION: "us-east-1", } as NodeJS.ProcessEnv, now: () => Number.NaN, tokenProviderFactory, }); expect(resolved).toEqual({ apiKey: "bedrock-api-key-invalid-clock", }); expect(tokenProvider).toHaveBeenCalledTimes(1); }); it("returns null for unsupported regions", async () => { const provider = await resolveImplicitMantleProvider({ env: { AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret AWS_REGION: "af-south-1", } as NodeJS.ProcessEnv, }); expect(provider).toBeNull(); }); it("defaults to us-east-1 when no region is set", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: "openai.gpt-oss-120b", object: "model" }] }), }); const provider = await resolveImplicitMantleProvider({ env: { AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret } as NodeJS.ProcessEnv, fetchFn: mockFetch as unknown as typeof fetch, }); expect(provider?.baseUrl).toBe("https://bedrock-mantle.us-east-1.api.aws/v1"); expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models"); objectArgAt(mockFetch, 0, 1); }); // --------------------------------------------------------------------------- // Provider merging // --------------------------------------------------------------------------- it("merges implicit models when existing provider has empty models", () => { const result = mergeImplicitMantleProvider({ existing: { baseUrl: "https://custom.example.com/v1", models: [], }, implicit: { baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1", api: "openai-completions", auth: "api-key", apiKey: "env:AWS_BEARER_TOKEN_BEDROCK", models: [ { id: "openai.gpt-oss-120b", name: "GPT-OSS 120B", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 4096, }, ], }, }); expect(result.baseUrl).toBe("https://custom.example.com/v1"); expect(result.models?.map((m) => m.id)).toEqual(["openai.gpt-oss-120b"]); }); it("preserves existing models over implicit ones", () => { const result = mergeImplicitMantleProvider({ existing: { baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1", models: [ { id: "custom-model", name: "My Custom Model", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 64000, maxTokens: 8192, }, ], }, implicit: { baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1", api: "openai-completions", auth: "api-key", models: [ { id: "openai.gpt-oss-120b", name: "GPT-OSS 120B", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 4096, }, ], }, }); expect(result.models?.map((m) => m.id)).toEqual(["custom-model"]); }); });