From 20fe8a6087ab7203d6dde1a9f69140beb1ce0eef Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 7 Aug 2026 11:08:15 -0400 Subject: [PATCH 1/3] feat: add config-bundle CLI commands --- src/core/configBundle.test.ts | 194 ++++++++ src/core/eval.tsx | 93 ++++ src/handlers/eval/config-bundle/components.ts | 31 ++ .../eval/config-bundle/config-bundle.test.tsx | 423 ++++++++++++++++++ .../eval/config-bundle/create/index.tsx | 50 +++ .../eval/config-bundle/delete/index.tsx | 22 + src/handlers/eval/config-bundle/get/index.tsx | 31 ++ src/handlers/eval/config-bundle/index.tsx | 21 + .../eval/config-bundle/list/index.tsx | 26 ++ .../eval/config-bundle/update/index.tsx | 61 +++ .../eval/config-bundle/version/index.tsx | 11 + .../eval/config-bundle/version/list/index.tsx | 33 ++ src/handlers/eval/index.tsx | 4 +- src/handlers/eval/types.tsx | 50 +++ src/testing/TestCoreClient.tsx | 149 ++++++ 15 files changed, 1198 insertions(+), 1 deletion(-) create mode 100644 src/core/configBundle.test.ts create mode 100644 src/handlers/eval/config-bundle/components.ts create mode 100644 src/handlers/eval/config-bundle/config-bundle.test.tsx create mode 100644 src/handlers/eval/config-bundle/create/index.tsx create mode 100644 src/handlers/eval/config-bundle/delete/index.tsx create mode 100644 src/handlers/eval/config-bundle/get/index.tsx create mode 100644 src/handlers/eval/config-bundle/index.tsx create mode 100644 src/handlers/eval/config-bundle/list/index.tsx create mode 100644 src/handlers/eval/config-bundle/update/index.tsx create mode 100644 src/handlers/eval/config-bundle/version/index.tsx create mode 100644 src/handlers/eval/config-bundle/version/list/index.tsx diff --git a/src/core/configBundle.test.ts b/src/core/configBundle.test.ts new file mode 100644 index 000000000..eac360b59 --- /dev/null +++ b/src/core/configBundle.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, test } from "bun:test"; +import { + CreateConfigurationBundleCommand, + DeleteConfigurationBundleCommand, + GetConfigurationBundleCommand, + GetConfigurationBundleVersionCommand, + ListConfigurationBundlesCommand, + ListConfigurationBundleVersionsCommand, + UpdateConfigurationBundleCommand, + type BedrockAgentCoreControlClient, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { NetworkingError } from "../errors"; +import { EvalClient } from "./eval"; +import type { AwsClients, ClientConfig } from "./types"; + +const OPTIONS = { region: "us-west-2", endpointUrl: "https://control.test" }; +const COMPONENTS = { + "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/orders-agent": { + configuration: { system_prompt: "Help with orders." }, + }, +}; + +function subject(respond: (command: unknown) => Promise): { + client: EvalClient; + configs: ClientConfig[]; +} { + const configs: ClientConfig[] = []; + const control = { send: respond } as unknown as BedrockAgentCoreControlClient; + const clients = { + control: (config: ClientConfig) => { + configs.push(config); + return control; + }, + } as unknown as AwsClients; + return { client: new EvalClient(clients), configs }; +} + +describe("EvalClient configuration bundles", () => { + test("create sends CreateConfigurationBundleCommand unchanged", async () => { + const sent: unknown[] = []; + const response = { + bundleArn: "arn:bundle:b-1", + bundleId: "b-1", + versionId: "v-1", + createdAt: new Date("2026-08-07T00:00:00Z"), + }; + const { client, configs } = subject(async (command) => { + sent.push(command); + return response; + }); + const input = { + bundleName: "orders-prompt", + components: COMPONENTS, + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/abc", + }; + + expect(await client.createConfigurationBundle(input, OPTIONS)).toBe(response); + expect(sent[0]).toBeInstanceOf(CreateConfigurationBundleCommand); + expect((sent[0] as CreateConfigurationBundleCommand).input).toEqual(input); + expect(configs).toEqual([{ region: "us-west-2", endpoint: "https://control.test" }]); + }); + + test("get selects the latest or immutable-version SDK operation", async () => { + const sent: unknown[] = []; + const { client } = subject(async (command) => { + sent.push(command); + return {}; + }); + + await client.getConfigurationBundle("b-1", undefined, OPTIONS); + await client.getConfigurationBundle("b-1", "v-2", OPTIONS); + + expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); + expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); + expect(sent[1]).toBeInstanceOf(GetConfigurationBundleVersionCommand); + expect((sent[1] as GetConfigurationBundleVersionCommand).input).toEqual({ + bundleId: "b-1", + versionId: "v-2", + }); + }); + + test("list sends only the aligned pagination fields", async () => { + const sent: unknown[] = []; + const { client } = subject(async (command) => { + sent.push(command); + return { bundles: [] }; + }); + + await client.listConfigurationBundles("token-1", 10, OPTIONS); + + expect(sent[0]).toBeInstanceOf(ListConfigurationBundlesCommand); + expect((sent[0] as ListConfigurationBundlesCommand).input).toEqual({ + nextToken: "token-1", + maxResults: 10, + }); + }); + + test("update gets the latest version and sends it as the sole parent", async () => { + const sent: unknown[] = []; + const response = { + bundleArn: "arn:bundle:b-1", + bundleId: "b-1", + versionId: "v-3", + updatedAt: new Date("2026-08-07T00:00:00Z"), + }; + const { client } = subject(async (command) => { + sent.push(command); + if (command instanceof GetConfigurationBundleCommand) { + return { + versionId: "v-2", + lineageMetadata: { branchName: "custom-branch" }, + }; + } + return response; + }); + + expect( + await client.updateConfigurationBundle( + "b-1", + { + components: COMPONENTS, + commitMessage: "Replace order support configuration", + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", + }, + OPTIONS, + ), + ).toBe(response); + + expect(sent).toHaveLength(2); + expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); + expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); + expect(sent[1]).toBeInstanceOf(UpdateConfigurationBundleCommand); + expect((sent[1] as UpdateConfigurationBundleCommand).input).toEqual({ + bundleId: "b-1", + components: COMPONENTS, + commitMessage: "Replace order support configuration", + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", + parentVersionIds: ["v-2"], + }); + }); + + test("update fails before sending when latest has no version id", async () => { + const sent: unknown[] = []; + const { client } = subject(async (command) => { + sent.push(command); + return {}; + }); + + const promise = client.updateConfigurationBundle( + "b-1", + { + components: COMPONENTS, + commitMessage: "Replace order support configuration", + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", + }, + OPTIONS, + ); + + await expect(promise).rejects.toBeInstanceOf(NetworkingError); + await expect(promise).rejects.toThrow(/returned no latest version/); + expect(sent).toHaveLength(1); + expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); + }); + + test("delete sends DeleteConfigurationBundleCommand", async () => { + const sent: unknown[] = []; + const response = { bundleId: "b-1", status: "DELETING" as const }; + const { client } = subject(async (command) => { + sent.push(command); + return response; + }); + + expect(await client.deleteConfigurationBundle("b-1", OPTIONS)).toBe(response); + expect(sent[0]).toBeInstanceOf(DeleteConfigurationBundleCommand); + expect((sent[0] as DeleteConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); + }); + + test("version list sends the parent bundle and pagination fields", async () => { + const sent: unknown[] = []; + const { client } = subject(async (command) => { + sent.push(command); + return { versions: [] }; + }); + + await client.listConfigurationBundleVersions("b-1", "token-1", 5, OPTIONS); + + expect(sent[0]).toBeInstanceOf(ListConfigurationBundleVersionsCommand); + expect((sent[0] as ListConfigurationBundleVersionsCommand).input).toEqual({ + bundleId: "b-1", + nextToken: "token-1", + maxResults: 5, + }); + }); +}); diff --git a/src/core/eval.tsx b/src/core/eval.tsx index c7e1baaa8..d83831754 100644 --- a/src/core/eval.tsx +++ b/src/core/eval.tsx @@ -1,38 +1,52 @@ import { + CreateConfigurationBundleCommand, CreateDatasetCommand, CreateDatasetVersionCommand, CreateEvaluatorCommand, CreateOnlineEvaluationConfigCommand, + DeleteConfigurationBundleCommand, DeleteDatasetCommand, DeleteEvaluatorCommand, DeleteOnlineEvaluationConfigCommand, + GetConfigurationBundleCommand, + GetConfigurationBundleVersionCommand, GetAgentRuntimeCommand, GetDatasetCommand, GetEvaluatorCommand, GetHarnessCommand, GetOnlineEvaluationConfigCommand, + ListConfigurationBundlesCommand, + ListConfigurationBundleVersionsCommand, ListDatasetsCommand, ListEvaluatorsCommand, ListOnlineEvaluationConfigsCommand, + UpdateConfigurationBundleCommand, UpdateEvaluatorCommand, UpdateOnlineEvaluationConfigCommand, + type CreateConfigurationBundleResponse, type CreateDatasetResponse, type CreateDatasetVersionResponse, type CreateEvaluatorRequest, type CreateEvaluatorResponse, type CreateOnlineEvaluationConfigResponse, + type DeleteConfigurationBundleResponse, type DeleteDatasetResponse, type DeleteEvaluatorResponse, type DeleteOnlineEvaluationConfigResponse, type EvaluatorConfig, + type GetConfigurationBundleResponse, + type GetConfigurationBundleVersionResponse, type GetDatasetResponse, type GetEvaluatorResponse, type GetOnlineEvaluationConfigResponse, + type ListConfigurationBundlesResponse, + type ListConfigurationBundleVersionsResponse, type ListDatasetsResponse, type ListEvaluatorsResponse, type DataSourceConfig, type ListOnlineEvaluationConfigsResponse, type Rule, + type UpdateConfigurationBundleResponse, type UpdateEvaluatorResponse, type UpdateOnlineEvaluationConfigResponse, type BedrockAgentCoreControlClient, @@ -53,12 +67,14 @@ import type { CodeBasedUpdate, RoleScopeWarning, CoreEvalClient, + CreateConfigurationBundleInput, CreateDatasetInput, CreateOnlineEvalInput, GetBatchEvaluationResult, LlmAsAJudgeUpdate, SessionSourceValue, StartBatchEvaluationInput, + UpdateConfigurationBundleInput, UpdateOnlineEvalInput, } from "../handlers/eval/types"; import { atomicWriteStream } from "../io"; @@ -601,6 +617,83 @@ export class EvalClient implements CoreEvalClient { .send(new DeleteOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id })); } + async createConfigurationBundle( + input: CreateConfigurationBundleInput, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new CreateConfigurationBundleCommand(input)); + } + + async getConfigurationBundle( + id: string, + version: string | undefined, + options: CoreOptions, + ): Promise { + const control = this.clients.control(toClientConfig(options)); + return version === undefined + ? control.send(new GetConfigurationBundleCommand({ bundleId: id })) + : control.send( + new GetConfigurationBundleVersionCommand({ bundleId: id, versionId: version }), + ); + } + + async listConfigurationBundles( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListConfigurationBundlesCommand({ nextToken, maxResults })); + } + + async updateConfigurationBundle( + id: string, + update: UpdateConfigurationBundleInput, + options: CoreOptions, + ): Promise { + const control = this.clients.control(toClientConfig(options)); + const current = await control.send(new GetConfigurationBundleCommand({ bundleId: id })); + if (!current.versionId) { + throw new NetworkingError( + `Configuration bundle "${id}" returned no latest version and cannot be updated`, + { meta: { bundleId: id } }, + ); + } + + return control.send( + new UpdateConfigurationBundleCommand({ + bundleId: id, + components: update.components, + commitMessage: update.commitMessage, + kmsKeyArn: update.kmsKeyArn, + parentVersionIds: [current.versionId], + }), + ); + } + + async deleteConfigurationBundle( + id: string, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new DeleteConfigurationBundleCommand({ bundleId: id })); + } + + async listConfigurationBundleVersions( + id: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListConfigurationBundleVersionsCommand({ bundleId: id, nextToken, maxResults })); + } + async createDataset( input: CreateDatasetInput, options: CoreOptions, diff --git a/src/handlers/eval/config-bundle/components.ts b/src/handlers/eval/config-bundle/components.ts new file mode 100644 index 000000000..7941c1dd0 --- /dev/null +++ b/src/handlers/eval/config-bundle/components.ts @@ -0,0 +1,31 @@ +import z from "zod"; +import type { ComponentConfiguration } from "@aws-sdk/client-bedrock-agentcore-control"; +import type { SourceResolver } from "../../../io"; +import { InputValidationError } from "../../../errors"; +import { parseJsonFlagWithSchema } from "../../utils"; + +const componentConfigurationSchema = z + .object({ + configuration: z.unknown().refine((value) => value !== undefined, "configuration is required"), + }) + .strict(); + +const componentMapSchema = z + .record(z.string().min(1), componentConfigurationSchema) + .refine((components) => Object.keys(components).length > 0, { + message: "must contain at least one component", + }); + +export type ConfigurationBundleComponents = Record; + +export async function resolveConfigurationBundleComponents( + value: string, + source: SourceResolver, +): Promise { + const text = await source.resolveText("components", value); + const components = parseJsonFlagWithSchema("components", text, componentMapSchema); + if (components === undefined) { + throw new InputValidationError("required option '--components ' not specified"); + } + return components as ConfigurationBundleComponents; +} diff --git a/src/handlers/eval/config-bundle/config-bundle.test.tsx b/src/handlers/eval/config-bundle/config-bundle.test.tsx new file mode 100644 index 000000000..dda683ab7 --- /dev/null +++ b/src/handlers/eval/config-bundle/config-bundle.test.tsx @@ -0,0 +1,423 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; +import type { CreateConfigurationBundleInput, UpdateConfigurationBundleInput } from "../types"; + +const REGION = "us-west-2"; +const COMPONENT_ARN = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/orders-agent-abc123"; +const COMPONENTS = { + [COMPONENT_ARN]: { + configuration: { + system_prompt: "You are an order-support assistant.", + settings: { cite_sources: true }, + }, + }, +}; + +const dirs: string[] = []; +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function writeTempJson(value: unknown): Promise { + const dir = mkdtempSync(join(tmpdir(), "agentcore-config-bundle-")); + dirs.push(dir); + const path = join(dir, "components.json"); + await Bun.write(path, JSON.stringify(value)); + return path; +} + +function testConfigBundleCommand(stdin?: string) { + const core = new TestCoreClient(); + const io = testIO(); + if (stdin !== undefined) { + io.io.stdin.push(stdin); + io.io.stdin.push(null); + } + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + return { + core, + stdout: io.stdout, + route: (args: string[]) => root.route(["bun", "agentcore", ...args, "--region", REGION]), + }; +} + +function callArgs(core: TestCoreClient, method: string): unknown[] { + const call = core.eval.calls.find((candidate) => candidate.method === method); + if (!call) throw new Error(`${method} was not called`); + return call.args; +} + +describe("eval config-bundle command hierarchy", () => { + test("registers CRUDL and nested version list commands", () => { + const root = createRootHandler(new TestCoreClient(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const configBundle = root + .children() + .find((child) => child.name() === "eval") + ?.children() + .find((child) => child.name() === "config-bundle"); + + expect(configBundle?.children().map((child) => child.name())).toEqual([ + "create", + "get", + "list", + "update", + "delete", + "version", + ]); + expect( + configBundle + ?.children() + .find((child) => child.name() === "version") + ?.children() + .map((child) => child.name()), + ).toEqual(["list"]); + expect( + configBundle + ?.children() + .find((child) => child.name() === "create") + ?.flags() + .map((candidate) => candidate.name), + ).toEqual(["name", "components", "kms-key-arn"]); + expect( + configBundle + ?.children() + .find((child) => child.name() === "update") + ?.flags() + .map((candidate) => candidate.name), + ).toEqual(["id", "components", "commit-message", "kms-key-arn"]); + }); + + test("prints help for a bare config-bundle command", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + + await route(["eval", "config-bundle", "--json"]); + + expect(stdout()).toContain("Usage: agentcore eval config-bundle"); + expect(core.eval.calls).toHaveLength(0); + }); +}); + +describe("config-bundle create", () => { + test("accepts an inline component map and renders the SDK response directly", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + core.eval.setCreateConfigurationBundleResponse({ + bundleArn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:configuration-bundle/b-1", + bundleId: "b-1", + versionId: "v-1", + createdAt: new Date("2026-08-07T00:00:00Z"), + }); + + await route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + JSON.stringify(COMPONENTS), + "--kms-key-arn", + "arn:aws:kms:us-west-2:123456789012:key/abc", + ]); + + expect(callArgs(core, "createConfigurationBundle")[0]).toEqual({ + bundleName: "orders-prompt", + components: COMPONENTS, + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/abc", + } satisfies CreateConfigurationBundleInput); + expect(JSON.parse(stdout())).toMatchObject({ + bundleArn: expect.any(String), + bundleId: "b-1", + versionId: "v-1", + }); + }); + + test("reads components from stdin", async () => { + const { core, route } = testConfigBundleCommand(JSON.stringify(COMPONENTS)); + + await route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + "-", + ]); + + expect(callArgs(core, "createConfigurationBundle")[0]).toMatchObject({ + components: COMPONENTS, + }); + }); + + test.each([ + ["an empty map", {}], + ["a component without configuration", { [COMPONENT_ARN]: {} }], + [ + "an unexpected component field", + { [COMPONENT_ARN]: { configuration: {}, description: "not accepted" } }, + ], + ])("rejects %s", async (_name, contents) => { + const { core, route } = testConfigBundleCommand(); + + await expect( + route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + JSON.stringify(contents), + ]), + ).rejects.toThrow(/Invalid value for option '--components'/); + expect(core.eval.calls).toHaveLength(0); + }); + + test("rejects malformed component JSON", async () => { + const { core, route } = testConfigBundleCommand(); + + await expect( + route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + "{not-json", + ]), + ).rejects.toThrow(/Invalid JSON for option '--components'/); + expect(core.eval.calls).toHaveLength(0); + }); + + test("requires both --name and --components", async () => { + const { core, route } = testConfigBundleCommand(); + + await expect( + route(["eval", "config-bundle", "create", "--components", JSON.stringify(COMPONENTS)]), + ).rejects.toThrow(/--name/); + await expect( + route(["eval", "config-bundle", "create", "--name", "orders-prompt"]), + ).rejects.toThrow(/--components/); + expect(core.eval.calls).toHaveLength(0); + }); +}); + +describe("config-bundle get", () => { + test("gets the latest bundle when --version is absent", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + core.eval.setGetConfigurationBundleResponse({ + bundleId: "b-1", + bundleArn: "arn:bundle:b-1", + bundleName: "orders-prompt", + versionId: "latest-v", + components: COMPONENTS, + createdAt: new Date("2026-08-06T00:00:00Z"), + updatedAt: new Date("2026-08-07T00:00:00Z"), + }); + + await route(["eval", "config-bundle", "get", "--id", "b-1"]); + + expect(callArgs(core, "getConfigurationBundle").slice(0, 2)).toEqual(["b-1", undefined]); + expect(JSON.parse(stdout()).versionId).toBe("latest-v"); + }); + + test("passes an explicit version through unchanged", async () => { + const { core, route } = testConfigBundleCommand(); + + await route(["eval", "config-bundle", "get", "--id", "b-1", "--version", "v-2"]); + + expect(callArgs(core, "getConfigurationBundle").slice(0, 2)).toEqual(["b-1", "v-2"]); + }); +}); + +describe("config-bundle list", () => { + test("passes pagination flags and renders the unmodified response", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + core.eval.setListConfigurationBundlesResponse( + { + bundles: [ + { + bundleArn: "arn:bundle:b-2", + bundleId: "b-2", + bundleName: "second-page", + }, + ], + nextToken: "token-2", + }, + "token-1", + ); + + await route(["eval", "config-bundle", "list", "--max-results", "1", "--next-token", "token-1"]); + + expect(callArgs(core, "listConfigurationBundles").slice(0, 2)).toEqual(["token-1", 1]); + expect(JSON.parse(stdout())).toMatchObject({ + bundles: [{ bundleId: "b-2", bundleName: "second-page" }], + nextToken: "token-2", + }); + }); +}); + +describe("config-bundle update", () => { + test("passes a complete replacement component map and KMS key", async () => { + const path = await writeTempJson(COMPONENTS); + const { core, route } = testConfigBundleCommand(); + + await route([ + "eval", + "config-bundle", + "update", + "--id", + "b-1", + "--components", + `file://${path}`, + "--commit-message", + "Replace order support configuration", + "--kms-key-arn", + "arn:aws:kms:us-west-2:123456789012:key/replacement", + ]); + + expect(callArgs(core, "updateConfigurationBundle").slice(0, 2)).toEqual([ + "b-1", + { + components: COMPONENTS, + commitMessage: "Replace order support configuration", + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/replacement", + } satisfies UpdateConfigurationBundleInput, + ]); + }); + + test("requires components even when a KMS key is provided", async () => { + const { core, route } = testConfigBundleCommand(); + + await expect( + route([ + "eval", + "config-bundle", + "update", + "--id", + "b-1", + "--commit-message", + "Rotate encryption key", + "--kms-key-arn", + "arn:aws:kms:us-west-2:123456789012:key/replacement", + ]), + ).rejects.toThrow(/required option '--components ' not specified/); + expect(core.eval.calls).toHaveLength(0); + }); + + test("requires a commit message", async () => { + const path = await writeTempJson(COMPONENTS); + const { core, route } = testConfigBundleCommand(); + + await expect( + route(["eval", "config-bundle", "update", "--id", "b-1", "--components", `file://${path}`]), + ).rejects.toThrow(/required option '--commit-message ' not specified/); + expect(core.eval.calls).toHaveLength(0); + }); + + test("requires an id", async () => { + const path = await writeTempJson(COMPONENTS); + const { core, route } = testConfigBundleCommand(); + + await expect( + route([ + "eval", + "config-bundle", + "update", + "--components", + `file://${path}`, + "--commit-message", + "Replace order support configuration", + ]), + ).rejects.toThrow(/required option '--id ' not specified/); + expect(core.eval.calls).toHaveLength(0); + }); +}); + +describe("config-bundle delete", () => { + test("takes only --id and renders the SDK response", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + core.eval.setDeleteConfigurationBundleResponse({ bundleId: "b-1", status: "DELETING" }); + + await route(["eval", "config-bundle", "delete", "--id", "b-1"]); + + expect(callArgs(core, "deleteConfigurationBundle")[0]).toBe("b-1"); + expect(JSON.parse(stdout())).toEqual({ bundleId: "b-1", status: "DELETING" }); + + const root = createRootHandler(new TestCoreClient(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const deleteCommand = root + .children() + .find((child) => child.name() === "eval") + ?.children() + .find((child) => child.name() === "config-bundle") + ?.children() + .find((child) => child.name() === "delete"); + expect(deleteCommand?.flags().map((candidate) => candidate.name)).toEqual(["id"]); + }); +}); + +describe("config-bundle version list", () => { + test("passes the bundle id and pagination flags", async () => { + const { core, stdout, route } = testConfigBundleCommand(); + core.eval.setListConfigurationBundleVersionsResponse( + { + versions: [ + { + bundleArn: "arn:bundle:b-1", + bundleId: "b-1", + versionId: "v-2", + versionCreatedAt: new Date("2026-08-07T00:00:00Z"), + }, + ], + }, + "token-1", + ); + + await route([ + "eval", + "config-bundle", + "version", + "list", + "--id", + "b-1", + "--max-results", + "5", + "--next-token", + "token-1", + ]); + + expect(callArgs(core, "listConfigurationBundleVersions").slice(0, 3)).toEqual([ + "b-1", + "token-1", + 5, + ]); + expect(JSON.parse(stdout()).versions).toEqual([ + expect.objectContaining({ bundleId: "b-1", versionId: "v-2" }), + ]); + }); +}); diff --git a/src/handlers/eval/config-bundle/create/index.tsx b/src/handlers/eval/config-bundle/create/index.tsx new file mode 100644 index 000000000..b5224bc9e --- /dev/null +++ b/src/handlers/eval/config-bundle/create/index.tsx @@ -0,0 +1,50 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { SourceResolver, type AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import { resolveConfigurationBundleComponents } from "../components"; + +export const createCreateConfigBundleHandler = (core: Core, io: AppIO) => + createHandler({ + name: "create", + description: "create a configuration bundle and its initial immutable version", + flags: [ + flag("name", "the name of the configuration bundle", z.string().optional()), + flag( + "components", + "complete component configuration map (JSON inline, file://, or - for stdin)", + z.string().optional(), + ), + flag( + "kms-key-arn", + "customer managed KMS key ARN for component configurations", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["name"]) { + throw new InputValidationError("required option '--name ' not specified"); + } + if (!flags["components"]) { + throw new InputValidationError("required option '--components ' not specified"); + } + + const components = await resolveConfigurationBundleComponents( + flags["components"], + new SourceResolver({ stdin: io.stdin }), + ); + ctx.require(JsonRendererKey).renderJson( + await core.eval.createConfigurationBundle( + { + bundleName: flags["name"], + components, + kmsKeyArn: flags["kms-key-arn"], + }, + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/config-bundle/delete/index.tsx b/src/handlers/eval/config-bundle/delete/index.tsx new file mode 100644 index 000000000..4e7066647 --- /dev/null +++ b/src/handlers/eval/config-bundle/delete/index.tsx @@ -0,0 +1,22 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createDeleteConfigBundleHandler = (core: Core) => + createHandler({ + name: "delete", + description: "delete a configuration bundle and all of its versions", + flags: [flag("id", "the ID of the configuration bundle", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags["id"]) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson(await core.eval.deleteConfigurationBundle(flags["id"], coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/eval/config-bundle/get/index.tsx b/src/handlers/eval/config-bundle/get/index.tsx new file mode 100644 index 000000000..153a43e2f --- /dev/null +++ b/src/handlers/eval/config-bundle/get/index.tsx @@ -0,0 +1,31 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetConfigBundleHandler = (core: Core) => + createHandler({ + name: "get", + description: "get the latest or a specific configuration bundle version", + flags: [ + flag("id", "the ID of the configuration bundle", z.string().optional()), + flag("version", "the immutable version ID to retrieve", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["id"]) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.getConfigurationBundle( + flags["id"], + flags["version"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/config-bundle/index.tsx b/src/handlers/eval/config-bundle/index.tsx new file mode 100644 index 000000000..6accd1f89 --- /dev/null +++ b/src/handlers/eval/config-bundle/index.tsx @@ -0,0 +1,21 @@ +import { Router } from "../../../router"; +import type { AppIO } from "../../../io"; +import type { Core } from "../../types"; +import { createHelpDefault } from "../../help"; +import { createCreateConfigBundleHandler } from "./create"; +import { createDeleteConfigBundleHandler } from "./delete"; +import { createGetConfigBundleHandler } from "./get"; +import { createListConfigBundlesHandler } from "./list"; +import { createUpdateConfigBundleHandler } from "./update"; +import { createConfigBundleVersionHandler } from "./version"; + +export function createConfigBundleHandler(core: Core, io: AppIO): Router { + return new Router("config-bundle", "manage AgentCore configuration bundles") + .default(createHelpDefault(io)) + .handler(createCreateConfigBundleHandler(core, io)) + .handler(createGetConfigBundleHandler(core)) + .handler(createListConfigBundlesHandler(core)) + .handler(createUpdateConfigBundleHandler(core, io)) + .handler(createDeleteConfigBundleHandler(core)) + .handler(createConfigBundleVersionHandler(core, io)); +} diff --git a/src/handlers/eval/config-bundle/list/index.tsx b/src/handlers/eval/config-bundle/list/index.tsx new file mode 100644 index 000000000..6c064883e --- /dev/null +++ b/src/handlers/eval/config-bundle/list/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListConfigBundlesHandler = (core: Core) => + createHandler({ + name: "list", + description: "list configuration bundles", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.listConfigurationBundles( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/config-bundle/update/index.tsx b/src/handlers/eval/config-bundle/update/index.tsx new file mode 100644 index 000000000..d06e99965 --- /dev/null +++ b/src/handlers/eval/config-bundle/update/index.tsx @@ -0,0 +1,61 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { SourceResolver, type AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import { resolveConfigurationBundleComponents } from "../components"; + +export const createUpdateConfigBundleHandler = (core: Core, io: AppIO) => + createHandler({ + name: "update", + description: "create a new immutable configuration bundle version", + flags: [ + flag("id", "the ID of the configuration bundle", z.string().optional()), + flag( + "components", + "replacement component configuration map (JSON inline, file://, or - for stdin)", + z.string().optional(), + ), + flag( + "commit-message", + "message describing the configuration bundle update", + z.string().max(500).optional(), + ), + flag( + "kms-key-arn", + "customer managed KMS key ARN to rotate component encryption to", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["id"]) { + throw new InputValidationError("required option '--id ' not specified"); + } + if (!flags["components"]) { + throw new InputValidationError("required option '--components ' not specified"); + } + if (!flags["commit-message"]) { + throw new InputValidationError( + "required option '--commit-message ' not specified", + ); + } + + const components = await resolveConfigurationBundleComponents( + flags["components"], + new SourceResolver({ stdin: io.stdin }), + ); + ctx.require(JsonRendererKey).renderJson( + await core.eval.updateConfigurationBundle( + flags["id"], + { + components, + commitMessage: flags["commit-message"], + kmsKeyArn: flags["kms-key-arn"], + }, + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/config-bundle/version/index.tsx b/src/handlers/eval/config-bundle/version/index.tsx new file mode 100644 index 000000000..ba848ed41 --- /dev/null +++ b/src/handlers/eval/config-bundle/version/index.tsx @@ -0,0 +1,11 @@ +import { Router } from "../../../../router"; +import type { AppIO } from "../../../../io"; +import type { Core } from "../../../types"; +import { createHelpDefault } from "../../../help"; +import { createListConfigBundleVersionsHandler } from "./list"; + +export function createConfigBundleVersionHandler(core: Core, io: AppIO): Router { + return new Router("version", "inspect immutable configuration bundle versions") + .default(createHelpDefault(io)) + .handler(createListConfigBundleVersionsHandler(core)); +} diff --git a/src/handlers/eval/config-bundle/version/list/index.tsx b/src/handlers/eval/config-bundle/version/list/index.tsx new file mode 100644 index 000000000..1a5e9f04f --- /dev/null +++ b/src/handlers/eval/config-bundle/version/list/index.tsx @@ -0,0 +1,33 @@ +import z from "zod"; +import { InputValidationError } from "../../../../../errors"; +import { createHandler, flag } from "../../../../../router"; +import { JsonRendererKey } from "../../../../../tui"; +import type { Core } from "../../../../types"; +import { coreOptsFromCtx } from "../../../../utils"; + +export const createListConfigBundleVersionsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list immutable versions of a configuration bundle", + flags: [ + flag("id", "the ID of the configuration bundle", z.string().optional()), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["id"]) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.listConfigurationBundleVersions( + flags["id"], + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/index.tsx b/src/handlers/eval/index.tsx index da3e6c686..5562ce2ce 100644 --- a/src/handlers/eval/index.tsx +++ b/src/handlers/eval/index.tsx @@ -7,6 +7,7 @@ import { createEvaluatorHandler } from "./evaluator"; import { createOnlineEvalHandler } from "./online-eval"; import { createDatasetHandler } from "./dataset"; import { createBatchEvaluationHandler } from "./batch-evaluation"; +import { createConfigBundleHandler } from "./config-bundle"; export function createEvalHandler(core: Core, io: AppIO): Router { return new Router("eval", "evaluate and optimize AgentCore agents") @@ -15,7 +16,8 @@ export function createEvalHandler(core: Core, io: AppIO): Router { .handler(createEvaluatorHandler(core, io)) .handler(createOnlineEvalHandler(core, io)) .handler(createDatasetHandler(core, io)) - .handler(createBatchEvaluationHandler(core, io)); + .handler(createBatchEvaluationHandler(core, io)) + .handler(createConfigBundleHandler(core, io)); } export { EvalScreen } from "./screen.tsx"; diff --git a/src/handlers/eval/types.tsx b/src/handlers/eval/types.tsx index 4a12e554d..0a654e925 100644 --- a/src/handlers/eval/types.tsx +++ b/src/handlers/eval/types.tsx @@ -1,22 +1,31 @@ import type { + CreateConfigurationBundleRequest, + CreateConfigurationBundleResponse, CreateDatasetRequest, CreateDatasetResponse, CreateDatasetVersionResponse, CreateEvaluatorRequest, CreateEvaluatorResponse, CreateOnlineEvaluationConfigResponse, + DeleteConfigurationBundleResponse, DeleteDatasetResponse, DeleteEvaluatorResponse, DeleteOnlineEvaluationConfigResponse, + GetConfigurationBundleResponse, + GetConfigurationBundleVersionResponse, GetDatasetResponse, GetEvaluatorResponse, GetOnlineEvaluationConfigResponse, + ListConfigurationBundlesResponse, + ListConfigurationBundleVersionsResponse, ListDatasetsResponse, ListEvaluatorsResponse, ListOnlineEvaluationConfigsResponse, DataSourceConfig, RatingScale, Rule, + UpdateConfigurationBundleRequest, + UpdateConfigurationBundleResponse, UpdateEvaluatorResponse, UpdateOnlineEvaluationConfigResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; @@ -172,6 +181,14 @@ export type RoleScopeWarning = { }; export type CreateDatasetInput = CreateDatasetRequest; +export type CreateConfigurationBundleInput = Pick< + CreateConfigurationBundleRequest, + "bundleName" | "components" | "kmsKeyArn" +>; +export type UpdateConfigurationBundleInput = Required< + Pick +> & + Pick; // StartBatchEvaluationInput is the CLI-facing shape for `batch-evaluation // evaluate`. Core turns `source` into the API's dataSourceConfig union and @@ -270,6 +287,39 @@ export interface CoreEvalClient { options: CoreOptions, ): Promise; + createConfigurationBundle( + input: CreateConfigurationBundleInput, + options: CoreOptions, + ): Promise; + // Omitting version returns the latest mainline version; an explicit version + // selects the immutable version API. + getConfigurationBundle( + id: string, + version: string | undefined, + options: CoreOptions, + ): Promise; + listConfigurationBundles( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + // Updates are appended to the latest mainline version by the Core client. + updateConfigurationBundle( + id: string, + update: UpdateConfigurationBundleInput, + options: CoreOptions, + ): Promise; + deleteConfigurationBundle( + id: string, + options: CoreOptions, + ): Promise; + listConfigurationBundleVersions( + id: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + // createDataset seeds a new dataset's DRAFT from `source`, which is required. // `schemaType` governs the structure of every example and is immutable after creation. // The response reports status CREATING — ingestion is asynchronous, and the dataset is not diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index df3414166..32f72ebf4 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -35,21 +35,28 @@ import type { ListGatewayRulesResponse, ListGatewaysResponse, ListGatewayTargetsResponse, + CreateConfigurationBundleResponse, CreateDatasetResponse, CreateDatasetVersionResponse, CreateEvaluatorRequest, CreateEvaluatorResponse, CreateOnlineEvaluationConfigResponse, + DeleteConfigurationBundleResponse, DeleteDatasetResponse, DeleteEvaluatorResponse, DeleteOnlineEvaluationConfigResponse, + GetConfigurationBundleResponse, + GetConfigurationBundleVersionResponse, GetDatasetResponse, + ListConfigurationBundlesResponse, + ListConfigurationBundleVersionsResponse, ListDatasetsResponse, GetEvaluatorResponse, GetOnlineEvaluationConfigResponse, ListEvaluatorsResponse, ListOnlineEvaluationConfigsResponse, MemoryView, + UpdateConfigurationBundleResponse, UpdateEvaluatorResponse, UpdateOnlineEvaluationConfigResponse, UpdateApiKeyCredentialProviderResponse, @@ -108,11 +115,13 @@ import type { BatchEvaluationResultEntry, CodeBasedUpdate, CoreEvalClient, + CreateConfigurationBundleInput, CreateDatasetInput, CreateOnlineEvalInput, GetBatchEvaluationResult, LlmAsAJudgeUpdate, StartBatchEvaluationInput, + UpdateConfigurationBundleInput, UpdateOnlineEvalInput, } from "../handlers/eval/types"; import { isTerminalStatus } from "../core/batchEvaluationResults"; @@ -210,6 +219,15 @@ const DEFAULT_CREATE_ONLINE_EVAL_RESPONSE = {} as CreateOnlineEvaluationConfigRe const DEFAULT_UPDATE_ONLINE_EVAL_RESPONSE = {} as UpdateOnlineEvaluationConfigResponse; const DEFAULT_GET_ONLINE_EVAL_RESPONSE = {} as GetOnlineEvaluationConfigResponse; const DEFAULT_DELETE_ONLINE_EVAL_RESPONSE = {} as DeleteOnlineEvaluationConfigResponse; +const DEFAULT_CREATE_CONFIG_BUNDLE_RESPONSE = {} as CreateConfigurationBundleResponse; +const DEFAULT_GET_CONFIG_BUNDLE_RESPONSE = {} as GetConfigurationBundleResponse; +const DEFAULT_GET_CONFIG_BUNDLE_VERSION_RESPONSE = {} as GetConfigurationBundleVersionResponse; +const DEFAULT_LIST_CONFIG_BUNDLES_RESPONSE: ListConfigurationBundlesResponse = { bundles: [] }; +const DEFAULT_UPDATE_CONFIG_BUNDLE_RESPONSE = {} as UpdateConfigurationBundleResponse; +const DEFAULT_DELETE_CONFIG_BUNDLE_RESPONSE = {} as DeleteConfigurationBundleResponse; +const DEFAULT_LIST_CONFIG_BUNDLE_VERSIONS_RESPONSE: ListConfigurationBundleVersionsResponse = { + versions: [], +}; const DEFAULT_CREATE_DATASET_RESPONSE = {} as CreateDatasetResponse; const DEFAULT_GET_DATASET_RESPONSE = {} as GetDatasetResponse; const DEFAULT_LIST_DATASETS_RESPONSE: ListDatasetsResponse = { datasets: [] }; @@ -1209,6 +1227,24 @@ export class TestEvalClient implements CoreEvalClient { DEFAULT_GET_ONLINE_EVAL_RESPONSE; private onlineEvalDeleteResponse: DeleteOnlineEvaluationConfigResponse = DEFAULT_DELETE_ONLINE_EVAL_RESPONSE; + private createConfigBundleResponse: CreateConfigurationBundleResponse = + DEFAULT_CREATE_CONFIG_BUNDLE_RESPONSE; + private getConfigBundleResponse: GetConfigurationBundleResponse = + DEFAULT_GET_CONFIG_BUNDLE_RESPONSE; + private getConfigBundleVersionResponse: GetConfigurationBundleVersionResponse = + DEFAULT_GET_CONFIG_BUNDLE_VERSION_RESPONSE; + private configBundleListResponses = new Map< + string | undefined, + ListConfigurationBundlesResponse + >(); + private updateConfigBundleResponse: UpdateConfigurationBundleResponse = + DEFAULT_UPDATE_CONFIG_BUNDLE_RESPONSE; + private deleteConfigBundleResponse: DeleteConfigurationBundleResponse = + DEFAULT_DELETE_CONFIG_BUNDLE_RESPONSE; + private configBundleVersionListResponses = new Map< + string | undefined, + ListConfigurationBundleVersionsResponse + >(); private createDatasetResponse: CreateDatasetResponse = DEFAULT_CREATE_DATASET_RESPONSE; private getDatasetResponse: GetDatasetResponse = DEFAULT_GET_DATASET_RESPONSE; private datasetListResponses = new Map(); @@ -1295,6 +1331,47 @@ export class TestEvalClient implements CoreEvalClient { return this; } + setCreateConfigurationBundleResponse(response: CreateConfigurationBundleResponse): this { + this.createConfigBundleResponse = response; + return this; + } + + setGetConfigurationBundleResponse(response: GetConfigurationBundleResponse): this { + this.getConfigBundleResponse = response; + return this; + } + + setGetConfigurationBundleVersionResponse(response: GetConfigurationBundleVersionResponse): this { + this.getConfigBundleVersionResponse = response; + return this; + } + + setListConfigurationBundlesResponse( + response: ListConfigurationBundlesResponse, + forNextToken?: string, + ): this { + this.configBundleListResponses.set(forNextToken, response); + return this; + } + + setUpdateConfigurationBundleResponse(response: UpdateConfigurationBundleResponse): this { + this.updateConfigBundleResponse = response; + return this; + } + + setDeleteConfigurationBundleResponse(response: DeleteConfigurationBundleResponse): this { + this.deleteConfigBundleResponse = response; + return this; + } + + setListConfigurationBundleVersionsResponse( + response: ListConfigurationBundleVersionsResponse, + forNextToken?: string, + ): this { + this.configBundleVersionListResponses.set(forNextToken, response); + return this; + } + // setCreateDatasetResponse sets what createDataset resolves to (when not // erroring). setCreateDatasetResponse(response: CreateDatasetResponse): this { @@ -1531,6 +1608,78 @@ export class TestEvalClient implements CoreEvalClient { return this.onlineEvalDeleteResponse; } + async createConfigurationBundle( + input: CreateConfigurationBundleInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "createConfigurationBundle", args: [input, options] }); + if (this.error) throw this.error; + return this.createConfigBundleResponse; + } + + async getConfigurationBundle( + id: string, + version: string | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "getConfigurationBundle", args: [id, version, options] }); + if (this.error) throw this.error; + return version === undefined + ? this.getConfigBundleResponse + : this.getConfigBundleVersionResponse; + } + + async listConfigurationBundles( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "listConfigurationBundles", args: [nextToken, maxResults, options] }); + if (this.error) throw this.error; + return ( + this.configBundleListResponses.get(nextToken) ?? + this.configBundleListResponses.get(undefined) ?? + DEFAULT_LIST_CONFIG_BUNDLES_RESPONSE + ); + } + + async updateConfigurationBundle( + id: string, + update: UpdateConfigurationBundleInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "updateConfigurationBundle", args: [id, update, options] }); + if (this.error) throw this.error; + return this.updateConfigBundleResponse; + } + + async deleteConfigurationBundle( + id: string, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "deleteConfigurationBundle", args: [id, options] }); + if (this.error) throw this.error; + return this.deleteConfigBundleResponse; + } + + async listConfigurationBundleVersions( + id: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "listConfigurationBundleVersions", + args: [id, nextToken, maxResults, options], + }); + if (this.error) throw this.error; + return ( + this.configBundleVersionListResponses.get(nextToken) ?? + this.configBundleVersionListResponses.get(undefined) ?? + DEFAULT_LIST_CONFIG_BUNDLE_VERSIONS_RESPONSE + ); + } + async createDataset( input: CreateDatasetInput, options: CoreOptions, From 4f027e91f762e8c177d19ca6fdddd25ce515264e Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Wed, 12 Aug 2026 09:57:44 -0400 Subject: [PATCH 2/3] feat: add branch-name flag to config-bundle operations --- src/core/configBundle.test.ts | 21 +++++-- src/core/eval.tsx | 8 ++- .../eval/config-bundle/config-bundle.test.tsx | 56 ++++++++++++++++++- src/handlers/eval/config-bundle/get/index.tsx | 6 ++ .../eval/config-bundle/update/index.tsx | 2 + src/handlers/eval/types.tsx | 9 +-- src/testing/TestCoreClient.tsx | 3 +- 7 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/core/configBundle.test.ts b/src/core/configBundle.test.ts index eac360b59..25eaac047 100644 --- a/src/core/configBundle.test.ts +++ b/src/core/configBundle.test.ts @@ -67,11 +67,14 @@ describe("EvalClient configuration bundles", () => { return {}; }); - await client.getConfigurationBundle("b-1", undefined, OPTIONS); - await client.getConfigurationBundle("b-1", "v-2", OPTIONS); + await client.getConfigurationBundle("b-1", undefined, "review-branch", OPTIONS); + await client.getConfigurationBundle("b-1", "v-2", "mainline", OPTIONS); expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); - expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); + expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ + bundleId: "b-1", + branchName: "review-branch", + }); expect(sent[1]).toBeInstanceOf(GetConfigurationBundleVersionCommand); expect((sent[1] as GetConfigurationBundleVersionCommand).input).toEqual({ bundleId: "b-1", @@ -118,6 +121,7 @@ describe("EvalClient configuration bundles", () => { await client.updateConfigurationBundle( "b-1", { + branchName: "review-branch", components: COMPONENTS, commitMessage: "Replace order support configuration", kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", @@ -128,10 +132,14 @@ describe("EvalClient configuration bundles", () => { expect(sent).toHaveLength(2); expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); - expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); + expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ + bundleId: "b-1", + branchName: "review-branch", + }); expect(sent[1]).toBeInstanceOf(UpdateConfigurationBundleCommand); expect((sent[1] as UpdateConfigurationBundleCommand).input).toEqual({ bundleId: "b-1", + branchName: "review-branch", components: COMPONENTS, commitMessage: "Replace order support configuration", kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", @@ -149,6 +157,7 @@ describe("EvalClient configuration bundles", () => { const promise = client.updateConfigurationBundle( "b-1", { + branchName: "mainline", components: COMPONENTS, commitMessage: "Replace order support configuration", kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/new", @@ -160,6 +169,10 @@ describe("EvalClient configuration bundles", () => { await expect(promise).rejects.toThrow(/returned no latest version/); expect(sent).toHaveLength(1); expect(sent[0]).toBeInstanceOf(GetConfigurationBundleCommand); + expect((sent[0] as GetConfigurationBundleCommand).input).toEqual({ + bundleId: "b-1", + branchName: "mainline", + }); }); test("delete sends DeleteConfigurationBundleCommand", async () => { diff --git a/src/core/eval.tsx b/src/core/eval.tsx index d83831754..67ff124aa 100644 --- a/src/core/eval.tsx +++ b/src/core/eval.tsx @@ -629,11 +629,12 @@ export class EvalClient implements CoreEvalClient { async getConfigurationBundle( id: string, version: string | undefined, + branchName: string, options: CoreOptions, ): Promise { const control = this.clients.control(toClientConfig(options)); return version === undefined - ? control.send(new GetConfigurationBundleCommand({ bundleId: id })) + ? control.send(new GetConfigurationBundleCommand({ bundleId: id, branchName })) : control.send( new GetConfigurationBundleVersionCommand({ bundleId: id, versionId: version }), ); @@ -655,7 +656,9 @@ export class EvalClient implements CoreEvalClient { options: CoreOptions, ): Promise { const control = this.clients.control(toClientConfig(options)); - const current = await control.send(new GetConfigurationBundleCommand({ bundleId: id })); + const current = await control.send( + new GetConfigurationBundleCommand({ bundleId: id, branchName: update.branchName }), + ); if (!current.versionId) { throw new NetworkingError( `Configuration bundle "${id}" returned no latest version and cannot be updated`, @@ -666,6 +669,7 @@ export class EvalClient implements CoreEvalClient { return control.send( new UpdateConfigurationBundleCommand({ bundleId: id, + branchName: update.branchName, components: update.components, commitMessage: update.commitMessage, kmsKeyArn: update.kmsKeyArn, diff --git a/src/handlers/eval/config-bundle/config-bundle.test.tsx b/src/handlers/eval/config-bundle/config-bundle.test.tsx index dda683ab7..a5cbb37d2 100644 --- a/src/handlers/eval/config-bundle/config-bundle.test.tsx +++ b/src/handlers/eval/config-bundle/config-bundle.test.tsx @@ -98,13 +98,20 @@ describe("eval config-bundle command hierarchy", () => { ?.flags() .map((candidate) => candidate.name), ).toEqual(["name", "components", "kms-key-arn"]); + expect( + configBundle + ?.children() + .find((child) => child.name() === "get") + ?.flags() + .map((candidate) => candidate.name), + ).toEqual(["id", "version", "branch-name"]); expect( configBundle ?.children() .find((child) => child.name() === "update") ?.flags() .map((candidate) => candidate.name), - ).toEqual(["id", "components", "commit-message", "kms-key-arn"]); + ).toEqual(["id", "components", "commit-message", "branch-name", "kms-key-arn"]); }); test("prints help for a bare config-bundle command", async () => { @@ -238,7 +245,11 @@ describe("config-bundle get", () => { await route(["eval", "config-bundle", "get", "--id", "b-1"]); - expect(callArgs(core, "getConfigurationBundle").slice(0, 2)).toEqual(["b-1", undefined]); + expect(callArgs(core, "getConfigurationBundle").slice(0, 3)).toEqual([ + "b-1", + undefined, + "mainline", + ]); expect(JSON.parse(stdout()).versionId).toBe("latest-v"); }); @@ -247,7 +258,23 @@ describe("config-bundle get", () => { await route(["eval", "config-bundle", "get", "--id", "b-1", "--version", "v-2"]); - expect(callArgs(core, "getConfigurationBundle").slice(0, 2)).toEqual(["b-1", "v-2"]); + expect(callArgs(core, "getConfigurationBundle").slice(0, 3)).toEqual([ + "b-1", + "v-2", + "mainline", + ]); + }); + + test("gets the latest version from an explicit branch", async () => { + const { core, route } = testConfigBundleCommand(); + + await route(["eval", "config-bundle", "get", "--id", "b-1", "--branch-name", "review-branch"]); + + expect(callArgs(core, "getConfigurationBundle").slice(0, 3)).toEqual([ + "b-1", + undefined, + "review-branch", + ]); }); }); @@ -300,6 +327,7 @@ describe("config-bundle update", () => { expect(callArgs(core, "updateConfigurationBundle").slice(0, 2)).toEqual([ "b-1", { + branchName: "mainline", components: COMPONENTS, commitMessage: "Replace order support configuration", kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/replacement", @@ -307,6 +335,28 @@ describe("config-bundle update", () => { ]); }); + test("updates an explicit branch", async () => { + const { core, route } = testConfigBundleCommand(); + + await route([ + "eval", + "config-bundle", + "update", + "--id", + "b-1", + "--components", + JSON.stringify(COMPONENTS), + "--commit-message", + "Update review branch configuration", + "--branch-name", + "review-branch", + ]); + + expect(callArgs(core, "updateConfigurationBundle")[1]).toMatchObject({ + branchName: "review-branch", + }); + }); + test("requires components even when a KMS key is provided", async () => { const { core, route } = testConfigBundleCommand(); diff --git a/src/handlers/eval/config-bundle/get/index.tsx b/src/handlers/eval/config-bundle/get/index.tsx index 153a43e2f..f0dc1811c 100644 --- a/src/handlers/eval/config-bundle/get/index.tsx +++ b/src/handlers/eval/config-bundle/get/index.tsx @@ -12,6 +12,11 @@ export const createGetConfigBundleHandler = (core: Core) => flags: [ flag("id", "the ID of the configuration bundle", z.string().optional()), flag("version", "the immutable version ID to retrieve", z.string().optional()), + flag( + "branch-name", + "branch used when retrieving the latest version", + z.string().default("mainline"), + ), ], handle: async (ctx, flags) => { if (!flags["id"]) { @@ -24,6 +29,7 @@ export const createGetConfigBundleHandler = (core: Core) => await core.eval.getConfigurationBundle( flags["id"], flags["version"], + flags["branch-name"], coreOptsFromCtx(ctx), ), ); diff --git a/src/handlers/eval/config-bundle/update/index.tsx b/src/handlers/eval/config-bundle/update/index.tsx index d06e99965..da360f6ad 100644 --- a/src/handlers/eval/config-bundle/update/index.tsx +++ b/src/handlers/eval/config-bundle/update/index.tsx @@ -23,6 +23,7 @@ export const createUpdateConfigBundleHandler = (core: Core, io: AppIO) => "message describing the configuration bundle update", z.string().max(500).optional(), ), + flag("branch-name", "branch to update", z.string().default("mainline")), flag( "kms-key-arn", "customer managed KMS key ARN to rotate component encryption to", @@ -50,6 +51,7 @@ export const createUpdateConfigBundleHandler = (core: Core, io: AppIO) => await core.eval.updateConfigurationBundle( flags["id"], { + branchName: flags["branch-name"], components, commitMessage: flags["commit-message"], kmsKeyArn: flags["kms-key-arn"], diff --git a/src/handlers/eval/types.tsx b/src/handlers/eval/types.tsx index 0a654e925..3abd4b7ae 100644 --- a/src/handlers/eval/types.tsx +++ b/src/handlers/eval/types.tsx @@ -186,7 +186,7 @@ export type CreateConfigurationBundleInput = Pick< "bundleName" | "components" | "kmsKeyArn" >; export type UpdateConfigurationBundleInput = Required< - Pick + Pick > & Pick; @@ -291,11 +291,12 @@ export interface CoreEvalClient { input: CreateConfigurationBundleInput, options: CoreOptions, ): Promise; - // Omitting version returns the latest mainline version; an explicit version - // selects the immutable version API. + // Omitting version returns the latest version on branchName; an explicit + // version selects the immutable version API. getConfigurationBundle( id: string, version: string | undefined, + branchName: string, options: CoreOptions, ): Promise; listConfigurationBundles( @@ -303,7 +304,7 @@ export interface CoreEvalClient { maxResults: number | undefined, options: CoreOptions, ): Promise; - // Updates are appended to the latest mainline version by the Core client. + // Updates are appended to the latest version on update.branchName. updateConfigurationBundle( id: string, update: UpdateConfigurationBundleInput, diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 32f72ebf4..660076413 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1620,9 +1620,10 @@ export class TestEvalClient implements CoreEvalClient { async getConfigurationBundle( id: string, version: string | undefined, + branchName: string, options: CoreOptions, ): Promise { - this.calls.push({ method: "getConfigurationBundle", args: [id, version, options] }); + this.calls.push({ method: "getConfigurationBundle", args: [id, version, branchName, options] }); if (this.error) throw this.error; return version === undefined ? this.getConfigBundleResponse From 7e7fd5a69e4695d0d6e6ca4402249fbfac99c049 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 13 Aug 2026 10:06:14 -0400 Subject: [PATCH 3/3] chore: add fixture tests and mark components flag sensitive --- src/core/configBundle.test.ts | 78 +------ ...gurationBundleCommand.516576a8849d86b.json | 8 + ...urationBundleCommand.d03693649c525e0e.json | 4 + ...urationBundleCommand.ddb5abdc145122fb.json | 4 + ...urationBundleCommand.1a198b076b8913d7.json | 26 +++ ...urationBundleCommand.be61ca94a6b48b63.json | 26 +++ ...BundleVersionCommand.38eddb7135e99e37.json | 26 +++ ...BundleVersionCommand.41340d2dc6e3e591.json | 29 +++ ...nBundleVersionCommand.8751d449316a710.json | 29 +++ ...BundleVersionCommand.d5181fb1b5533a9d.json | 26 +++ ...undleVersionsCommand.d03693649c525e0e.json | 31 +++ ...undleVersionsCommand.ddb5abdc145122fb.json | 31 +++ ...rationBundlesCommand.23f97c9dcdd6350b.json | 12 ++ ...urationBundleCommand.299ce52dbaeb1cc3.json | 8 + ...urationBundleCommand.3de2154dacb66880.json | 8 + .../config-bundle-create.golden.json | 6 + .../config-bundle-delete.golden.json | 4 + .../config-bundle-get-latest.golden.json | 22 ++ .../config-bundle-get-v1.golden.json | 22 ++ .../config-bundle-get-v2.golden.json | 25 +++ .../config-bundle-list.golden.json | 10 + .../config-bundle-update.golden.json | 6 + .../config-bundle-version-list.golden.json | 27 +++ .../config-bundle.fixture.test.tsx | 199 ++++++++++++++++++ .../eval/config-bundle/config-bundle.test.tsx | 116 ++-------- .../eval/config-bundle/create/index.tsx | 1 + .../eval/config-bundle/update/index.tsx | 1 + 27 files changed, 615 insertions(+), 170 deletions(-) create mode 100644 src/handlers/eval/config-bundle/__fixtures__/CreateConfigurationBundleCommand.516576a8849d86b.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.d03693649c525e0e.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.ddb5abdc145122fb.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.1a198b076b8913d7.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.be61ca94a6b48b63.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.38eddb7135e99e37.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.41340d2dc6e3e591.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.8751d449316a710.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.d5181fb1b5533a9d.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.d03693649c525e0e.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.ddb5abdc145122fb.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundlesCommand.23f97c9dcdd6350b.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.299ce52dbaeb1cc3.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.3de2154dacb66880.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-create.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-delete.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-latest.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v1.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v2.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-list.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-update.golden.json create mode 100644 src/handlers/eval/config-bundle/__fixtures__/config-bundle-version-list.golden.json create mode 100644 src/handlers/eval/config-bundle/config-bundle.fixture.test.tsx diff --git a/src/core/configBundle.test.ts b/src/core/configBundle.test.ts index 25eaac047..371e81523 100644 --- a/src/core/configBundle.test.ts +++ b/src/core/configBundle.test.ts @@ -1,11 +1,7 @@ import { describe, expect, test } from "bun:test"; import { - CreateConfigurationBundleCommand, - DeleteConfigurationBundleCommand, GetConfigurationBundleCommand, GetConfigurationBundleVersionCommand, - ListConfigurationBundlesCommand, - ListConfigurationBundleVersionsCommand, UpdateConfigurationBundleCommand, type BedrockAgentCoreControlClient, } from "@aws-sdk/client-bedrock-agentcore-control"; @@ -36,31 +32,7 @@ function subject(respond: (command: unknown) => Promise): { } describe("EvalClient configuration bundles", () => { - test("create sends CreateConfigurationBundleCommand unchanged", async () => { - const sent: unknown[] = []; - const response = { - bundleArn: "arn:bundle:b-1", - bundleId: "b-1", - versionId: "v-1", - createdAt: new Date("2026-08-07T00:00:00Z"), - }; - const { client, configs } = subject(async (command) => { - sent.push(command); - return response; - }); - const input = { - bundleName: "orders-prompt", - components: COMPONENTS, - kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/abc", - }; - - expect(await client.createConfigurationBundle(input, OPTIONS)).toBe(response); - expect(sent[0]).toBeInstanceOf(CreateConfigurationBundleCommand); - expect((sent[0] as CreateConfigurationBundleCommand).input).toEqual(input); - expect(configs).toEqual([{ region: "us-west-2", endpoint: "https://control.test" }]); - }); - - test("get selects the latest or immutable-version SDK operation", async () => { + test("get passes an explicit branch or selects the immutable-version operation", async () => { const sent: unknown[] = []; const { client } = subject(async (command) => { sent.push(command); @@ -82,23 +54,7 @@ describe("EvalClient configuration bundles", () => { }); }); - test("list sends only the aligned pagination fields", async () => { - const sent: unknown[] = []; - const { client } = subject(async (command) => { - sent.push(command); - return { bundles: [] }; - }); - - await client.listConfigurationBundles("token-1", 10, OPTIONS); - - expect(sent[0]).toBeInstanceOf(ListConfigurationBundlesCommand); - expect((sent[0] as ListConfigurationBundlesCommand).input).toEqual({ - nextToken: "token-1", - maxResults: 10, - }); - }); - - test("update gets the latest version and sends it as the sole parent", async () => { + test("update uses the same explicit branch for the parent lookup and update", async () => { const sent: unknown[] = []; const response = { bundleArn: "arn:bundle:b-1", @@ -174,34 +130,4 @@ describe("EvalClient configuration bundles", () => { branchName: "mainline", }); }); - - test("delete sends DeleteConfigurationBundleCommand", async () => { - const sent: unknown[] = []; - const response = { bundleId: "b-1", status: "DELETING" as const }; - const { client } = subject(async (command) => { - sent.push(command); - return response; - }); - - expect(await client.deleteConfigurationBundle("b-1", OPTIONS)).toBe(response); - expect(sent[0]).toBeInstanceOf(DeleteConfigurationBundleCommand); - expect((sent[0] as DeleteConfigurationBundleCommand).input).toEqual({ bundleId: "b-1" }); - }); - - test("version list sends the parent bundle and pagination fields", async () => { - const sent: unknown[] = []; - const { client } = subject(async (command) => { - sent.push(command); - return { versions: [] }; - }); - - await client.listConfigurationBundleVersions("b-1", "token-1", 5, OPTIONS); - - expect(sent[0]).toBeInstanceOf(ListConfigurationBundleVersionsCommand); - expect((sent[0] as ListConfigurationBundleVersionsCommand).input).toEqual({ - bundleId: "b-1", - nextToken: "token-1", - maxResults: 5, - }); - }); }); diff --git a/src/handlers/eval/config-bundle/__fixtures__/CreateConfigurationBundleCommand.516576a8849d86b.json b/src/handlers/eval/config-bundle/__fixtures__/CreateConfigurationBundleCommand.516576a8849d86b.json new file mode 100644 index 000000000..9c0bdd882 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/CreateConfigurationBundleCommand.516576a8849d86b.json @@ -0,0 +1,8 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "createdAt": { + "$date": "2026-08-13T14:04:44.109Z" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.d03693649c525e0e.json b/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.d03693649c525e0e.json new file mode 100644 index 000000000..cbfc45f94 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.d03693649c525e0e.json @@ -0,0 +1,4 @@ +{ + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.ddb5abdc145122fb.json b/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.ddb5abdc145122fb.json new file mode 100644 index 000000000..3caf9cebe --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/DeleteConfigurationBundleCommand.ddb5abdc145122fb.json @@ -0,0 +1,4 @@ +{ + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.1a198b076b8913d7.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.1a198b076b8913d7.json new file mode 100644 index 000000000..20386f625 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.1a198b076b8913d7.json @@ -0,0 +1,26 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "93a10f5c-2ffa-4895-93ff-f6b64b3515ad", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "updatedAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.be61ca94a6b48b63.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.be61ca94a6b48b63.json new file mode 100644 index 000000000..45f271d7d --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleCommand.be61ca94a6b48b63.json @@ -0,0 +1,26 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "updatedAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.38eddb7135e99e37.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.38eddb7135e99e37.json new file mode 100644 index 000000000..c25e4669f --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.38eddb7135e99e37.json @@ -0,0 +1,26 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "versionCreatedAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.41340d2dc6e3e591.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.41340d2dc6e3e591.json new file mode 100644 index 000000000..4c5e05e72 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.41340d2dc6e3e591.json @@ -0,0 +1,29 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 2 + }, + "system_prompt": "Configuration bundle fixture version two." + } + } + }, + "createdAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "versionCreatedAt": { + "$date": "2026-08-13T14:04:49.892Z" + }, + "lineageMetadata": { + "parentVersionIds": [ + "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.8751d449316a710.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.8751d449316a710.json new file mode 100644 index 000000000..05c131404 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.8751d449316a710.json @@ -0,0 +1,29 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "0c6c10e2-4351-443f-8423-3cc6283c011e", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 2 + }, + "system_prompt": "Configuration bundle fixture version two." + } + } + }, + "createdAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "versionCreatedAt": { + "$date": "2026-08-13T13:58:54.027Z" + }, + "lineageMetadata": { + "parentVersionIds": [ + "93a10f5c-2ffa-4895-93ff-f6b64b3515ad" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.d5181fb1b5533a9d.json b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.d5181fb1b5533a9d.json new file mode 100644 index 000000000..1d68d9df8 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/GetConfigurationBundleVersionCommand.d5181fb1b5533a9d.json @@ -0,0 +1,26 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "93a10f5c-2ffa-4895-93ff-f6b64b3515ad", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "versionCreatedAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.d03693649c525e0e.json b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.d03693649c525e0e.json new file mode 100644 index 000000000..73a9c097d --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.d03693649c525e0e.json @@ -0,0 +1,31 @@ +{ + "versions": [ + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "versionCreatedAt": { + "$date": "2026-08-13T14:04:49.892Z" + }, + "lineageMetadata": { + "parentVersionIds": [ + "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } + }, + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "versionCreatedAt": { + "$date": "2026-08-13T14:04:44.109Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.ddb5abdc145122fb.json b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.ddb5abdc145122fb.json new file mode 100644 index 000000000..195485686 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundleVersionsCommand.ddb5abdc145122fb.json @@ -0,0 +1,31 @@ +{ + "versions": [ + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "versionId": "0c6c10e2-4351-443f-8423-3cc6283c011e", + "versionCreatedAt": { + "$date": "2026-08-13T13:58:54.027Z" + }, + "lineageMetadata": { + "parentVersionIds": [ + "93a10f5c-2ffa-4895-93ff-f6b64b3515ad" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } + }, + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "versionId": "93a10f5c-2ffa-4895-93ff-f6b64b3515ad", + "versionCreatedAt": { + "$date": "2026-08-13T13:58:48.269Z" + }, + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundlesCommand.23f97c9dcdd6350b.json b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundlesCommand.23f97c9dcdd6350b.json new file mode 100644 index 000000000..ec36b3daa --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/ListConfigurationBundlesCommand.23f97c9dcdd6350b.json @@ -0,0 +1,12 @@ +{ + "bundles": [ + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "createdAt": { + "$date": "2026-08-13T14:04:44.109Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.299ce52dbaeb1cc3.json b/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.299ce52dbaeb1cc3.json new file mode 100644 index 000000000..da26be619 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.299ce52dbaeb1cc3.json @@ -0,0 +1,8 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "bundleId": "agentcore_cli_config_bundle_fixture-8R7L5N9Wyh", + "versionId": "0c6c10e2-4351-443f-8423-3cc6283c011e", + "updatedAt": { + "$date": "2026-08-13T13:58:54.027Z" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.3de2154dacb66880.json b/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.3de2154dacb66880.json new file mode 100644 index 000000000..c58aeeabe --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/UpdateConfigurationBundleCommand.3de2154dacb66880.json @@ -0,0 +1,8 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "updatedAt": { + "$date": "2026-08-13T14:04:49.892Z" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-create.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-create.golden.json new file mode 100644 index 000000000..271148353 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-create.golden.json @@ -0,0 +1,6 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "createdAt": "2026-08-13T14:04:44.109Z" +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-delete.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-delete.golden.json new file mode 100644 index 000000000..cbfc45f94 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-delete.golden.json @@ -0,0 +1,4 @@ +{ + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-latest.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-latest.golden.json new file mode 100644 index 000000000..4bb12708a --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-latest.golden.json @@ -0,0 +1,22 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": "2026-08-13T14:04:44.109Z", + "updatedAt": "2026-08-13T14:04:44.109Z", + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v1.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v1.golden.json new file mode 100644 index 000000000..cf1efb02f --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v1.golden.json @@ -0,0 +1,22 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 1 + }, + "system_prompt": "Configuration bundle fixture version one." + } + } + }, + "createdAt": "2026-08-13T14:04:44.109Z", + "versionCreatedAt": "2026-08-13T14:04:44.109Z", + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v2.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v2.golden.json new file mode 100644 index 000000000..048cbd12c --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-get-v2.golden.json @@ -0,0 +1,25 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "components": { + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp": { + "configuration": { + "settings": { + "revision": 2 + }, + "system_prompt": "Configuration bundle fixture version two." + } + } + }, + "createdAt": "2026-08-13T14:04:44.109Z", + "versionCreatedAt": "2026-08-13T14:04:49.892Z", + "lineageMetadata": { + "parentVersionIds": [ + "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-list.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-list.golden.json new file mode 100644 index 000000000..3df8ee08e --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-list.golden.json @@ -0,0 +1,10 @@ +{ + "bundles": [ + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleName": "agentcore_cli_config_bundle_fixture", + "createdAt": "2026-08-13T14:04:44.109Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-update.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-update.golden.json new file mode 100644 index 000000000..875da6679 --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-update.golden.json @@ -0,0 +1,6 @@ +{ + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "updatedAt": "2026-08-13T14:04:49.892Z" +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/__fixtures__/config-bundle-version-list.golden.json b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-version-list.golden.json new file mode 100644 index 000000000..4d2e5f16d --- /dev/null +++ b/src/handlers/eval/config-bundle/__fixtures__/config-bundle-version-list.golden.json @@ -0,0 +1,27 @@ +{ + "versions": [ + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c3eea665-5638-4c59-8c03-478180ecabcb", + "versionCreatedAt": "2026-08-13T14:04:49.892Z", + "lineageMetadata": { + "parentVersionIds": [ + "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc" + ], + "branchName": "mainline", + "commitMessage": "Record configuration bundle fixture version two" + } + }, + { + "bundleArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:configuration-bundle/agentcore_cli_config_bundle_fixture-s03KQeHM93", + "bundleId": "agentcore_cli_config_bundle_fixture-s03KQeHM93", + "versionId": "c67c8477-9b51-4bfc-92c4-36b7f2cdcbdc", + "versionCreatedAt": "2026-08-13T14:04:44.109Z", + "lineageMetadata": { + "parentVersionIds": [], + "branchName": "mainline" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/config-bundle/config-bundle.fixture.test.tsx b/src/handlers/eval/config-bundle/config-bundle.fixture.test.tsx new file mode 100644 index 000000000..a41bcb54c --- /dev/null +++ b/src/handlers/eval/config-bundle/config-bundle.fixture.test.tsx @@ -0,0 +1,199 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { + DeleteConfigurationBundleCommand, + GetConfigurationBundleCommand, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { CoreClient } from "../../../core"; +import { createControlClient } from "../../../core/factories"; +import { + createSilentLogger, + fixtureFactories, + isRecording, + matchGolden, + settle, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); +const BUNDLE_NAME = "agentcore_cli_config_bundle_fixture"; +const COMPONENT_ARN = + "arn:aws:bedrock-agentcore:us-west-2:314146320088:runtime/stmFixes_StmFixesAgent-XsFfJU4cAp"; +const COMPONENTS_V1 = { + [COMPONENT_ARN]: { + configuration: { + system_prompt: "Configuration bundle fixture version one.", + settings: { revision: 1 }, + }, + }, +}; +const COMPONENTS_V2 = { + [COMPONENT_ARN]: { + configuration: { + system_prompt: "Configuration bundle fixture version two.", + settings: { revision: 2 }, + }, + }, +}; + +// Record with: +// RECORD=1 bun test src/handlers/eval/config-bundle/config-bundle.fixture.test.tsx +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient, createLogsClient } = + fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + createLogsClient, + logger: createSilentLogger(), + }); +} + +async function run(args: string[]): Promise { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...args, "--region", REGION]); + return io.stdout(); +} + +let bundleId: string; +let firstVersionId: string; +let secondVersionId: string; + +afterAll(async () => { + if (!isRecording() || !bundleId) return; + + const control = createControlClient({ region: REGION }); + try { + await control.send(new GetConfigurationBundleCommand({ bundleId, branchName: "mainline" })); + await control.send(new DeleteConfigurationBundleCommand({ bundleId })); + } catch (error) { + if ((error as Error).name !== "ResourceNotFoundException") { + console.error(`could not clean up fixture configuration bundle ${bundleId}:`, error); + } + } +}); + +describe("eval config-bundle against recorded responses", () => { + test("creates a configuration bundle", async () => { + const stdout = await run([ + "eval", + "config-bundle", + "create", + "--name", + BUNDLE_NAME, + "--components", + JSON.stringify(COMPONENTS_V1), + ]); + + matchGolden(FIXTURES, "config-bundle-create.golden.json", stdout); + const response = JSON.parse(stdout); + bundleId = response.bundleId; + firstVersionId = response.versionId; + expect(bundleId).toBeString(); + expect(firstVersionId).toBeString(); + }); + + test("gets the latest mainline version", async () => { + await settle(); + + const stdout = await run(["eval", "config-bundle", "get", "--id", bundleId]); + + matchGolden(FIXTURES, "config-bundle-get-latest.golden.json", stdout); + const response = JSON.parse(stdout); + expect(response.versionId).toBe(firstVersionId); + expect(response.components).toEqual(COMPONENTS_V1); + expect(response.lineageMetadata.branchName).toBe("mainline"); + }, 60_000); + + test("gets the initial immutable version", async () => { + const stdout = await run([ + "eval", + "config-bundle", + "get", + "--id", + bundleId, + "--version", + firstVersionId, + ]); + + matchGolden(FIXTURES, "config-bundle-get-v1.golden.json", stdout); + expect(JSON.parse(stdout).components).toEqual(COMPONENTS_V1); + }); + + test("lists configuration bundles", async () => { + const stdout = await run(["eval", "config-bundle", "list", "--json"]); + + matchGolden(FIXTURES, "config-bundle-list.golden.json", stdout); + expect(JSON.parse(stdout).bundles).toEqual( + expect.arrayContaining([expect.objectContaining({ bundleId })]), + ); + }); + + test("updates from the latest mainline parent", async () => { + const stdout = await run([ + "eval", + "config-bundle", + "update", + "--id", + bundleId, + "--components", + JSON.stringify(COMPONENTS_V2), + "--commit-message", + "Record configuration bundle fixture version two", + ]); + + matchGolden(FIXTURES, "config-bundle-update.golden.json", stdout); + const response = JSON.parse(stdout); + secondVersionId = response.versionId; + expect(secondVersionId).toBeString(); + expect(secondVersionId).not.toBe(firstVersionId); + }); + + test("gets the updated immutable version", async () => { + await settle(); + + const stdout = await run([ + "eval", + "config-bundle", + "get", + "--id", + bundleId, + "--version", + secondVersionId, + ]); + + matchGolden(FIXTURES, "config-bundle-get-v2.golden.json", stdout); + const response = JSON.parse(stdout); + expect(response.components).toEqual(COMPONENTS_V2); + expect(response.lineageMetadata.parentVersionIds).toEqual([firstVersionId]); + expect(response.lineageMetadata.commitMessage).toBe( + "Record configuration bundle fixture version two", + ); + }, 60_000); + + test("lists both immutable versions", async () => { + const stdout = await run(["eval", "config-bundle", "version", "list", "--id", bundleId]); + + matchGolden(FIXTURES, "config-bundle-version-list.golden.json", stdout); + expect( + JSON.parse(stdout).versions.map((version: { versionId: string }) => version.versionId), + ).toEqual(expect.arrayContaining([firstVersionId, secondVersionId])); + }); + + test("deletes the configuration bundle", async () => { + const stdout = await run(["eval", "config-bundle", "delete", "--id", bundleId]); + + matchGolden(FIXTURES, "config-bundle-delete.golden.json", stdout); + expect(JSON.parse(stdout)).toMatchObject({ bundleId, status: "DELETING" }); + }); +}); diff --git a/src/handlers/eval/config-bundle/config-bundle.test.tsx b/src/handlers/eval/config-bundle/config-bundle.test.tsx index a5cbb37d2..c04627686 100644 --- a/src/handlers/eval/config-bundle/config-bundle.test.tsx +++ b/src/handlers/eval/config-bundle/config-bundle.test.tsx @@ -10,7 +10,7 @@ import { testIO, } from "../../../testing"; import { createRootHandler } from "../../index"; -import type { CreateConfigurationBundleInput, UpdateConfigurationBundleInput } from "../types"; +import type { UpdateConfigurationBundleInput } from "../types"; const REGION = "us-west-2"; const COMPONENT_ARN = @@ -112,6 +112,27 @@ describe("eval config-bundle command hierarchy", () => { ?.flags() .map((candidate) => candidate.name), ).toEqual(["id", "components", "commit-message", "branch-name", "kms-key-arn"]); + expect( + configBundle + ?.children() + .find((child) => child.name() === "create") + ?.flags() + .find((candidate) => candidate.name === "components")?.sensitive, + ).toBe(true); + expect( + configBundle + ?.children() + .find((child) => child.name() === "update") + ?.flags() + .find((candidate) => candidate.name === "components")?.sensitive, + ).toBe(true); + expect( + configBundle + ?.children() + .find((child) => child.name() === "delete") + ?.flags() + .map((candidate) => candidate.name), + ).toEqual(["id"]); }); test("prints help for a bare config-bundle command", async () => { @@ -125,39 +146,6 @@ describe("eval config-bundle command hierarchy", () => { }); describe("config-bundle create", () => { - test("accepts an inline component map and renders the SDK response directly", async () => { - const { core, stdout, route } = testConfigBundleCommand(); - core.eval.setCreateConfigurationBundleResponse({ - bundleArn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:configuration-bundle/b-1", - bundleId: "b-1", - versionId: "v-1", - createdAt: new Date("2026-08-07T00:00:00Z"), - }); - - await route([ - "eval", - "config-bundle", - "create", - "--name", - "orders-prompt", - "--components", - JSON.stringify(COMPONENTS), - "--kms-key-arn", - "arn:aws:kms:us-west-2:123456789012:key/abc", - ]); - - expect(callArgs(core, "createConfigurationBundle")[0]).toEqual({ - bundleName: "orders-prompt", - components: COMPONENTS, - kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/abc", - } satisfies CreateConfigurationBundleInput); - expect(JSON.parse(stdout())).toMatchObject({ - bundleArn: expect.any(String), - bundleId: "b-1", - versionId: "v-1", - }); - }); - test("reads components from stdin", async () => { const { core, route } = testConfigBundleCommand(JSON.stringify(COMPONENTS)); @@ -231,40 +219,6 @@ describe("config-bundle create", () => { }); describe("config-bundle get", () => { - test("gets the latest bundle when --version is absent", async () => { - const { core, stdout, route } = testConfigBundleCommand(); - core.eval.setGetConfigurationBundleResponse({ - bundleId: "b-1", - bundleArn: "arn:bundle:b-1", - bundleName: "orders-prompt", - versionId: "latest-v", - components: COMPONENTS, - createdAt: new Date("2026-08-06T00:00:00Z"), - updatedAt: new Date("2026-08-07T00:00:00Z"), - }); - - await route(["eval", "config-bundle", "get", "--id", "b-1"]); - - expect(callArgs(core, "getConfigurationBundle").slice(0, 3)).toEqual([ - "b-1", - undefined, - "mainline", - ]); - expect(JSON.parse(stdout()).versionId).toBe("latest-v"); - }); - - test("passes an explicit version through unchanged", async () => { - const { core, route } = testConfigBundleCommand(); - - await route(["eval", "config-bundle", "get", "--id", "b-1", "--version", "v-2"]); - - expect(callArgs(core, "getConfigurationBundle").slice(0, 3)).toEqual([ - "b-1", - "v-2", - "mainline", - ]); - }); - test("gets the latest version from an explicit branch", async () => { const { core, route } = testConfigBundleCommand(); @@ -405,32 +359,6 @@ describe("config-bundle update", () => { }); }); -describe("config-bundle delete", () => { - test("takes only --id and renders the SDK response", async () => { - const { core, stdout, route } = testConfigBundleCommand(); - core.eval.setDeleteConfigurationBundleResponse({ bundleId: "b-1", status: "DELETING" }); - - await route(["eval", "config-bundle", "delete", "--id", "b-1"]); - - expect(callArgs(core, "deleteConfigurationBundle")[0]).toBe("b-1"); - expect(JSON.parse(stdout())).toEqual({ bundleId: "b-1", status: "DELETING" }); - - const root = createRootHandler(new TestCoreClient(), { - io: testIO().io, - logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), - }); - const deleteCommand = root - .children() - .find((child) => child.name() === "eval") - ?.children() - .find((child) => child.name() === "config-bundle") - ?.children() - .find((child) => child.name() === "delete"); - expect(deleteCommand?.flags().map((candidate) => candidate.name)).toEqual(["id"]); - }); -}); - describe("config-bundle version list", () => { test("passes the bundle id and pagination flags", async () => { const { core, stdout, route } = testConfigBundleCommand(); diff --git a/src/handlers/eval/config-bundle/create/index.tsx b/src/handlers/eval/config-bundle/create/index.tsx index b5224bc9e..784596468 100644 --- a/src/handlers/eval/config-bundle/create/index.tsx +++ b/src/handlers/eval/config-bundle/create/index.tsx @@ -17,6 +17,7 @@ export const createCreateConfigBundleHandler = (core: Core, io: AppIO) => "components", "complete component configuration map (JSON inline, file://, or - for stdin)", z.string().optional(), + { sensitive: true }, ), flag( "kms-key-arn", diff --git a/src/handlers/eval/config-bundle/update/index.tsx b/src/handlers/eval/config-bundle/update/index.tsx index da360f6ad..09ea79917 100644 --- a/src/handlers/eval/config-bundle/update/index.tsx +++ b/src/handlers/eval/config-bundle/update/index.tsx @@ -17,6 +17,7 @@ export const createUpdateConfigBundleHandler = (core: Core, io: AppIO) => "components", "replacement component configuration map (JSON inline, file://, or - for stdin)", z.string().optional(), + { sensitive: true }, ), flag( "commit-message",