Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
693
extensions/amazon-bedrock-mantle/discovery.test.ts
Normal file
693
extensions/amazon-bedrock-mantle/discovery.test.ts
Normal file
@@ -0,0 +1,693 @@
|
||||
// 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<string>) {
|
||||
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<string, unknown> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, unknown> {
|
||||
if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`expected ${field} to be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<string>>()
|
||||
.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<string>>()
|
||||
.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"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user