From 95ff7de1a7510aa754ae84743935748caedba98d Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 23 Sep 2026 21:44:16 +0000 Subject: [PATCH 01/11] feat(config): add standalone resource command availability setting --- src/globalConfig/config.tsx | 2 ++ src/globalConfig/types.tsx | 1 + src/handlers/config/config.test.tsx | 30 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/globalConfig/config.tsx b/src/globalConfig/config.tsx index 5b9eb2e494..8ef14b63f7 100644 --- a/src/globalConfig/config.tsx +++ b/src/globalConfig/config.tsx @@ -4,6 +4,7 @@ import type { DeepPartial, GlobalConfig } from "./types"; * Default values for the global config. Includes a unique installationId for each process. */ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { + "imperative-commands": false, "imperative-mutation-commands": false, telemetry: { enabled: true, @@ -22,6 +23,7 @@ export function applyOverrides( overrides: DeepPartial, ): GlobalConfig { return { + "imperative-commands": overrides["imperative-commands"] ?? defaults["imperative-commands"], "imperative-mutation-commands": overrides["imperative-mutation-commands"] ?? defaults["imperative-mutation-commands"], telemetry: { diff --git a/src/globalConfig/types.tsx b/src/globalConfig/types.tsx index 86288f1508..a6b3d3823f 100644 --- a/src/globalConfig/types.tsx +++ b/src/globalConfig/types.tsx @@ -12,6 +12,7 @@ export type DeepPartial = { [P in keyof T]?: DeepPartial }; * Schema for the global config file. All fields should be optional with defaults defined. */ export const globalConfigFileSchema = z.object({ + "imperative-commands": z.boolean().optional(), "imperative-mutation-commands": z.boolean().optional(), telemetry: z .object({ diff --git a/src/handlers/config/config.test.tsx b/src/handlers/config/config.test.tsx index b3e1b1f211..a0cc534b38 100644 --- a/src/handlers/config/config.test.tsx +++ b/src/handlers/config/config.test.tsx @@ -56,6 +56,36 @@ describe("config", () => { expect(JSON.parse(output)).toBe(true); }); + test("imperative command families default off for existing and missing config", async () => { + expect(JSON.parse(await run(["imperative-commands"]))).toBe(false); + await rm(configPath); + expect(JSON.parse(await run(["imperative-commands"]))).toBe(false); + }); + + test("persists the root gate independently of the existing Gateway gate", async () => { + await run(["imperative-mutation-commands", "true"]); + expect(JSON.parse(await run(["imperative-commands"]))).toBe(false); + for (const enabled of [true, false]) { + expect(JSON.parse(await run(["imperative-commands", String(enabled)]))).toBe(enabled); + expect(JSON.parse(await run(["imperative-commands"]))).toBe(enabled); + expect(JSON.parse(await run(["imperative-mutation-commands"]))).toBe(true); + } + }); + + test("rejects invalid root gate values without changing config", async () => { + await run(["imperative-commands", "true"]); + await expect(run(["imperative-commands", "invalid"])).rejects.toThrow(InputValidationError); + expect(JSON.parse(await run(["imperative-commands"]))).toBe(true); + }); + + test("validates the root gate when reading config from disk", async () => { + await writeFile( + configPath, + JSON.stringify({ ...validConfigOverrides, "imperative-commands": "true" }), + ); + await expect(run([])).rejects.toThrow("Failed to deserialize"); + }); + test("imperative mutations default to false for existing and missing config files", async () => { expect(JSON.parse(await run(["imperative-mutation-commands"]))).toBe(false); await rm(configPath); From b819a100cb85aadc0f2c6d9166f36436c4653b9a Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 23 Sep 2026 21:45:42 +0000 Subject: [PATCH 02/11] test(tui): opt resource screen suites into standalone commands --- src/components/PaginatedTablePicker.test.tsx | 8 +++++- .../ProjectResourceCreateScreen.test.tsx | 12 ++++----- src/components/Root.test.tsx | 10 +++++-- .../gateway/gateway.mutations.screen.test.tsx | 19 ++++++++++--- src/handlers/gateway/gateway.screen.test.tsx | 12 ++++++--- .../gateway/invoke/invoke.screen.test.tsx | 2 +- .../gateway/policy/generate.screen.test.tsx | 19 ++++++++++--- .../harness/create/create.screen.test.tsx | 2 +- .../harness/delete/delete.screen.test.tsx | 2 +- .../endpoint/create/create.screen.test.tsx | 10 ++++--- .../endpoint/delete/delete.screen.test.tsx | 10 ++++--- .../endpoint/list/list.screen.test.tsx | 6 +++-- .../endpoint/update/update.screen.test.tsx | 10 ++++--- .../harness/exec/exec.screen.test.tsx | 2 +- src/handlers/harness/get/get.screen.test.tsx | 2 +- .../harness/invoke/invoke.screen.test.tsx | 2 +- .../harness/list/list.screen.test.tsx | 2 +- .../harness/update/update.screen.test.tsx | 2 +- .../harness/version/list/list.screen.test.tsx | 10 ++++--- .../apikey.screen.test.tsx | 27 ++++++++++++++----- .../oauth2.screen.test.tsx | 27 ++++++++++++++----- .../memory/actor/actor.screen.test.tsx | 2 +- .../memory/event/event.screen.test.tsx | 2 +- src/handlers/memory/memory.screen.test.tsx | 2 +- .../memory/record/record.screen.test.tsx | 2 +- .../memory/session/session.screen.test.tsx | 15 ++++++++--- .../runtime/endpoint/endpoint.screen.test.tsx | 2 +- .../runtime/invoke/invoke.screen.test.tsx | 6 +++-- src/handlers/runtime/runtime.screen.test.tsx | 10 ++++--- .../runtime/shell/shell.screen.test.tsx | 2 +- .../runtime/version/version.screen.test.tsx | 2 +- src/testing/globalConfig.tsx | 5 ++++ src/testing/index.tsx | 3 ++- src/testing/renderScreen.tsx | 12 ++++++++- 34 files changed, 189 insertions(+), 72 deletions(-) diff --git a/src/components/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index 0a0269253a..b0ea23059a 100644 --- a/src/components/PaginatedTablePicker.test.tsx +++ b/src/components/PaginatedTablePicker.test.tsx @@ -6,7 +6,13 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import { QueryClient } from "@tanstack/react-query"; import stringWidth from "string-width"; -import { cleanupScreens, renderScreen, TestCoreClient, waitFor, waitForText } from "../testing"; +import { + cleanupScreens, + renderImperativeScreen as renderScreen, + TestCoreClient, + waitFor, + waitForText, +} from "../testing"; afterEach(cleanupScreens); diff --git a/src/components/ProjectResourceCreateScreen.test.tsx b/src/components/ProjectResourceCreateScreen.test.tsx index 92af129cf2..d78cc622ba 100644 --- a/src/components/ProjectResourceCreateScreen.test.tsx +++ b/src/components/ProjectResourceCreateScreen.test.tsx @@ -3,15 +3,15 @@ import { cleanupScreens, compiledRootCommand, menuEntries, - renderScreen, + renderImperativeScreen, + IMPERATIVE_GLOBAL_CONFIG, waitForText, } from "../testing"; import type { ProjectCreateResource } from "./ProjectResourceCreateScreen"; -import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; afterEach(cleanupScreens); -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { ...IMPERATIVE_GLOBAL_CONFIG, "imperative-mutation-commands": true }; const RESOURCES = [ { @@ -43,7 +43,7 @@ describe("project resource creation guidance", () => { test.each(RESOURCES)( "$resource lists create in its TUI menu and opens project instructions", async ({ resource, label, parentDescription, addCommand }) => { - const r = renderScreen(`/agentcore/${resource}`); + const r = renderImperativeScreen(`/agentcore/${resource}`); await waitForText(r.lastFrame, "❯ create"); expect(menuEntries(r.lastFrame()!).screens[0]).toBe("create"); @@ -65,7 +65,7 @@ describe("project resource creation guidance", () => { ); test("the guidance does not add unsupported imperative CLI commands", () => { - const root = compiledRootCommand(); + const root = compiledRootCommand(undefined, IMPERATIVE_GLOBAL_CONFIG); for (const resource of RESOURCES) { const command = root.commands.find((candidate) => candidate.name() === resource.resource); expect(command?.commands.some((candidate) => candidate.name() === "create")).toBe(false); @@ -75,7 +75,7 @@ describe("project resource creation guidance", () => { test.each(RESOURCES.filter(({ resource }) => resource !== "gateway"))( "$resource keeps project guidance when Gateway mutations are enabled", async ({ resource, label, addCommand }) => { - const r = renderScreen(`/agentcore/${resource}`, { globalConfig: MUTATION_CONFIG }); + const r = renderImperativeScreen(`/agentcore/${resource}`, { globalConfig: MUTATION_CONFIG }); await waitForText(r.lastFrame, "type to choose a command"); const entries = menuEntries(r.lastFrame()!); expect(entries.screens[0]).toBe("create"); diff --git a/src/components/Root.test.tsx b/src/components/Root.test.tsx index dd5532f9cd..a42fd10c4b 100644 --- a/src/components/Root.test.tsx +++ b/src/components/Root.test.tsx @@ -1,7 +1,13 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { Command } from "commander"; import { isTuiCommandSupported } from "../router"; -import { cleanupScreens, compiledRootCommand, renderScreen, waitFor } from "../testing"; +import { + cleanupScreens, + compiledRootCommand, + IMPERATIVE_GLOBAL_CONFIG, + renderImperativeScreen as renderScreen, + waitFor, +} from "../testing"; afterEach(cleanupScreens); @@ -41,7 +47,7 @@ function firstLine(frame: string | undefined): string { return (frame ?? "").split("\n")[0]?.trim() ?? ""; } -const SCREENS = screenCommands(compiledRootCommand(), []); +const SCREENS = screenCommands(compiledRootCommand(undefined, IMPERATIVE_GLOBAL_CONFIG), []); describe("every command with a screen", () => { test("there are screens to cover", () => { diff --git a/src/handlers/gateway/gateway.mutations.screen.test.tsx b/src/handlers/gateway/gateway.mutations.screen.test.tsx index 6b0b04941b..9c277e2a12 100644 --- a/src/handlers/gateway/gateway.mutations.screen.test.tsx +++ b/src/handlers/gateway/gateway.mutations.screen.test.tsx @@ -1,5 +1,10 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { cleanupScreens, menuEntries, renderScreen, waitForText } from "../../testing"; +import { + cleanupScreens, + menuEntries, + renderImperativeScreen as renderScreen, + waitForText, +} from "../../testing"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; afterEach(cleanupScreens); @@ -20,7 +25,11 @@ describe("Gateway mutation menus", () => { test.each(GROUPS)("%s preserves enabled CLI-only mutations", async (group) => { const screen = renderScreen(`/agentcore/${group}`, { - globalConfig: { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }, + globalConfig: { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, + }, }); await waitForText(screen.lastFrame, "command line only"); const entries = menuEntries(screen.lastFrame()!); @@ -50,7 +59,11 @@ describe("Gateway mutation menus", () => { test.each([false, true])("direct create route matches flag %s", async (enabled) => { const screen = renderScreen("/agentcore/gateway/create", { - globalConfig: { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": enabled }, + globalConfig: { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": enabled, + "imperative-commands": true, + }, }); await waitForText( screen.lastFrame, diff --git a/src/handlers/gateway/gateway.screen.test.tsx b/src/handlers/gateway/gateway.screen.test.tsx index fb1995a027..6aae5d648f 100644 --- a/src/handlers/gateway/gateway.screen.test.tsx +++ b/src/handlers/gateway/gateway.screen.test.tsx @@ -11,7 +11,7 @@ import { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitForText, menuEntries, @@ -124,7 +124,10 @@ describe("Gateway menu and list", () => { test("renders Gateway identity and calls list with exact Core options", async () => { const core = coreWithGateways([gateway()]); - const screen = renderScreen("/agentcore/gateway/list", { core, endpointUrl: ENDPOINT }); + const screen = renderScreen("/agentcore/gateway/list", { + core, + endpointUrl: ENDPOINT, + }); await waitForText(screen.lastFrame, "checkout-gateway"); const frame = screen.lastFrame()!; @@ -171,7 +174,10 @@ describe("Gateway menu and list", () => { test("selects a Gateway and renders only read-only hub actions", async () => { const core = coreWithGateways([gateway()]); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen("/agentcore/gateway/list", { core, endpointUrl: ENDPOINT }); + const screen = renderScreen("/agentcore/gateway/list", { + core, + endpointUrl: ENDPOINT, + }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); diff --git a/src/handlers/gateway/invoke/invoke.screen.test.tsx b/src/handlers/gateway/invoke/invoke.screen.test.tsx index a0cf2b1d19..ce00d93626 100644 --- a/src/handlers/gateway/invoke/invoke.screen.test.tsx +++ b/src/handlers/gateway/invoke/invoke.screen.test.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { GatewaySummary, GetGatewayResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/handlers/gateway/policy/generate.screen.test.tsx b/src/handlers/gateway/policy/generate.screen.test.tsx index 9b2621e2b6..7b8fb7b11d 100644 --- a/src/handlers/gateway/policy/generate.screen.test.tsx +++ b/src/handlers/gateway/policy/generate.screen.test.tsx @@ -1,6 +1,11 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { GatewaySummary, GetGatewayResponse } from "@aws-sdk/client-bedrock-agentcore-control"; -import { cleanupScreens, renderScreen, TestCoreClient, waitForText } from "../../../testing"; +import { + cleanupScreens, + renderImperativeScreen as renderScreen, + TestCoreClient, + waitForText, +} from "../../../testing"; afterEach(cleanupScreens); @@ -86,7 +91,9 @@ describe("gateway policy generate screen", () => { { statement: PERMIT, findings: [] }, ], }; - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core }); + const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + core, + }); await waitForText(screen.lastFrame, PLACEHOLDER); await screen.write("forbid IAM callers"); @@ -113,7 +120,9 @@ describe("gateway policy generate screen", () => { test("aborts the run and returns to the picker on esc while generating", async () => { const core = coreWith(ENGINE_ARN); core.policy.hang = true; - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core }); + const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + core, + }); await waitForText(screen.lastFrame, PLACEHOLDER); await screen.write("x"); @@ -128,7 +137,9 @@ describe("gateway policy generate screen", () => { test("shows the error and returns to the form on esc", async () => { const core = coreWith(ENGINE_ARN); core.policy.error = new Error("policy generation 'gen-1' failed: bad prompt"); - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core }); + const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + core, + }); await waitForText(screen.lastFrame, PLACEHOLDER); await screen.write("x"); diff --git a/src/handlers/harness/create/create.screen.test.tsx b/src/handlers/harness/create/create.screen.test.tsx index 8d73c8ce8b..9d03fcd8bf 100644 --- a/src/handlers/harness/create/create.screen.test.tsx +++ b/src/handlers/harness/create/create.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { CreateHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/delete/delete.screen.test.tsx b/src/handlers/harness/delete/delete.screen.test.tsx index 843fdc53f0..4b01964feb 100644 --- a/src/handlers/harness/delete/delete.screen.test.tsx +++ b/src/handlers/harness/delete/delete.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/endpoint/create/create.screen.test.tsx b/src/handlers/harness/endpoint/create/create.screen.test.tsx index 454dcd450f..83e1309f06 100644 --- a/src/handlers/harness/endpoint/create/create.screen.test.tsx +++ b/src/handlers/harness/endpoint/create/create.screen.test.tsx @@ -4,7 +4,7 @@ import type { HarnessVersionSummary, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, @@ -74,7 +74,9 @@ describe("harness endpoint create wizard", () => { test("walks name → version → review and creates", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { core }); + const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { + core, + }); await waitForText(r.lastFrame, "what should this endpoint be called?"); await r.write("prod"); @@ -121,7 +123,9 @@ describe("harness endpoint create wizard", () => { test("keeping `latest` omits targetVersion from the request", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { core }); + const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { + core, + }); await waitForText(r.lastFrame, "what should this endpoint be called?"); await r.write("prod"); diff --git a/src/handlers/harness/endpoint/delete/delete.screen.test.tsx b/src/handlers/harness/endpoint/delete/delete.screen.test.tsx index b79a5dc624..15286b79ba 100644 --- a/src/handlers/harness/endpoint/delete/delete.screen.test.tsx +++ b/src/handlers/harness/endpoint/delete/delete.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, @@ -66,7 +66,9 @@ describe("harness endpoint delete screen", () => { test("`y` calls DeleteHarnessEndpoint and shows the result", async () => { const core = coreWithEndpoint(); - const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { core }); + const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { + core, + }); await waitForText(r.lastFrame, "Delete endpoint prod?"); await r.write("y"); @@ -80,7 +82,9 @@ describe("harness endpoint delete screen", () => { test("`n` cancels without calling DeleteHarnessEndpoint", async () => { const core = coreWithEndpoint(); - const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { core }); + const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { + core, + }); await waitForText(r.lastFrame, "Delete endpoint prod?"); await r.write("n"); diff --git a/src/handlers/harness/endpoint/list/list.screen.test.tsx b/src/handlers/harness/endpoint/list/list.screen.test.tsx index 94c9920897..a2d41f3017 100644 --- a/src/handlers/harness/endpoint/list/list.screen.test.tsx +++ b/src/handlers/harness/endpoint/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, @@ -136,7 +136,9 @@ describe("harness endpoint list screen", () => { test("retries a failed endpoint detail without losing its selectors", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("endpoint unavailable")); - const r = renderScreen("/agentcore/harness/endpoint/get/MyHarness-abc123/prod", { core }); + const r = renderScreen("/agentcore/harness/endpoint/get/MyHarness-abc123/prod", { + core, + }); await waitForText(r.lastFrame, "endpoint unavailable"); expect(r.lastFrame()).toContain("[r] retry"); diff --git a/src/handlers/harness/endpoint/update/update.screen.test.tsx b/src/handlers/harness/endpoint/update/update.screen.test.tsx index e3eb1ee1fb..263dbeaada 100644 --- a/src/handlers/harness/endpoint/update/update.screen.test.tsx +++ b/src/handlers/harness/endpoint/update/update.screen.test.tsx @@ -4,7 +4,7 @@ import type { HarnessVersionSummary, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, @@ -85,7 +85,9 @@ describe("harness endpoint update wizard", () => { test("repointing at a new version submits only targetVersion", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { core }); + const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { + core, + }); // The endpoint's current target (version 1) is preselected; no "latest" // option exists in update mode. @@ -111,7 +113,9 @@ describe("harness endpoint update wizard", () => { test("keeping the version unchanged submits only the endpoint identity", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { core }); + const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { + core, + }); await waitForText(r.lastFrame, "● version 1"); await r.press("return"); // keep version diff --git a/src/handlers/harness/exec/exec.screen.test.tsx b/src/handlers/harness/exec/exec.screen.test.tsx index 90de531104..e2a3eb3fe2 100644 --- a/src/handlers/harness/exec/exec.screen.test.tsx +++ b/src/handlers/harness/exec/exec.screen.test.tsx @@ -6,7 +6,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse, HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, cleanupScreens, StreamController, diff --git a/src/handlers/harness/get/get.screen.test.tsx b/src/handlers/harness/get/get.screen.test.tsx index 7bf33fd55e..794ef5dc04 100644 --- a/src/handlers/harness/get/get.screen.test.tsx +++ b/src/handlers/harness/get/get.screen.test.tsx @@ -9,7 +9,7 @@ import type { Harness, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/invoke/invoke.screen.test.tsx b/src/handlers/harness/invoke/invoke.screen.test.tsx index 5591f94d1c..665ce587c9 100644 --- a/src/handlers/harness/invoke/invoke.screen.test.tsx +++ b/src/handlers/harness/invoke/invoke.screen.test.tsx @@ -5,7 +5,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse, HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/list/list.screen.test.tsx b/src/handlers/harness/list/list.screen.test.tsx index 54701e3843..f2640a7119 100644 --- a/src/handlers/harness/list/list.screen.test.tsx +++ b/src/handlers/harness/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/update/update.screen.test.tsx b/src/handlers/harness/update/update.screen.test.tsx index 40c8517741..503984e312 100644 --- a/src/handlers/harness/update/update.screen.test.tsx +++ b/src/handlers/harness/update/update.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, diff --git a/src/handlers/harness/version/list/list.screen.test.tsx b/src/handlers/harness/version/list/list.screen.test.tsx index bee5261d52..5f80b7665a 100644 --- a/src/handlers/harness/version/list/list.screen.test.tsx +++ b/src/handlers/harness/version/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessVersionSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderScreen, + renderImperativeScreen as renderScreen, waitForText, waitFor, cleanupScreens, @@ -133,7 +133,9 @@ describe("harness version list screen", () => { nextToken: "v2", }); core.harness.setListVersionsResponse({ harnessVersions: [] }, "v2"); - const laterPage = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); + const laterPage = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { + core, + }); await waitForText(laterPage.lastFrame, "page 1 · more →"); await laterPage.write("l"); @@ -170,7 +172,9 @@ describe("harness version list screen", () => { test("retries a failed version detail without losing its selectors", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("version unavailable")); - const r = renderScreen("/agentcore/harness/version/get/MyHarness-abc123/42", { core }); + const r = renderScreen("/agentcore/harness/version/get/MyHarness-abc123/42", { + core, + }); await waitForText(r.lastFrame, "version unavailable"); expect(r.lastFrame()).toContain("[r] retry"); diff --git a/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx b/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx index 1ba1b463ca..9fc8b4a82c 100644 --- a/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx +++ b/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, tick, waitFor, @@ -76,7 +76,9 @@ describe("API key credential provider picker", () => { lastUpdatedTime: new Date("2026-07-21T02:03:04.000Z"), }), ]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, "visible-provider"); const frame = screen.lastFrame()!; @@ -88,7 +90,10 @@ describe("API key credential provider picker", () => { test("calls listApiKeyCredentialProviders with exact Core options", async () => { const core = coreWithProviders([providerItem()]); - renderScreen("/agentcore/identity/api-key-credential-provider/list", { core, endpointUrl }); + renderScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + endpointUrl, + }); await waitFor(() => core.identity.calls.some((call) => call.method === "listApiKeyCredentialProviders"), @@ -105,7 +110,9 @@ describe("API key credential provider picker", () => { test("caps maxResults at the service limit of 20 on a tall terminal", async () => { const core = coreWithProviders([providerItem()]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + }); // Terminal taller than the 20-row service cap: page size must still clamp. await screen.resize(120, 60); @@ -129,7 +136,9 @@ describe("API key credential provider picker", () => { nextToken: "page-2", }); core.identity.setListApiKeyResponse({ credentialProviders: [] }, "page-2"); - const paged = renderScreen("/agentcore/identity/api-key-credential-provider/list", { core }); + const paged = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + }); await waitForText(paged.lastFrame, "page 1 · more →"); await paged.write("l"); @@ -141,7 +150,9 @@ describe("API key credential provider picker", () => { test("bare get redirects to the picker", async () => { const core = coreWithProviders([providerItem({ name: "redirected" })]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get", { core }); + const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get", { + core, + }); await waitForText(screen.lastFrame, "redirected"); expect(core.identity.calls[0]?.method).toBe("listApiKeyCredentialProviders"); @@ -151,7 +162,9 @@ describe("API key credential provider picker", () => { const name = "api key blue"; const core = coreWithProviders([providerItem({ name })]); core.identity.setGetApiKeyResponse(getResponse({ name })); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, name); await screen.press("return"); diff --git a/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx b/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx index 4429deb13d..09e6ee8df5 100644 --- a/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx +++ b/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, tick, waitFor, @@ -79,7 +79,9 @@ describe("OAuth2 credential provider picker", () => { lastUpdatedTime: new Date("2026-07-21T02:03:04.000Z"), }), ]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, "visible-provider"); const frame = screen.lastFrame()!; @@ -91,7 +93,10 @@ describe("OAuth2 credential provider picker", () => { test("calls listOauth2CredentialProviders with exact Core options", async () => { const core = coreWithProviders([providerItem()]); - renderScreen("/agentcore/identity/oauth2-credential-provider/list", { core, endpointUrl }); + renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + endpointUrl, + }); await waitFor(() => core.identity.calls.some((call) => call.method === "listOauth2CredentialProviders"), @@ -108,7 +113,9 @@ describe("OAuth2 credential provider picker", () => { test("caps maxResults at the service limit of 20 on a tall terminal", async () => { const core = coreWithProviders([providerItem()]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + }); // Terminal taller than the 20-row service cap: page size must still clamp. await screen.resize(120, 60); @@ -132,7 +139,9 @@ describe("OAuth2 credential provider picker", () => { nextToken: "page-2", }); core.identity.setListOauth2Response({ credentialProviders: [] }, "page-2"); - const paged = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { core }); + const paged = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + }); await waitForText(paged.lastFrame, "page 1 · more →"); await paged.write("l"); @@ -142,7 +151,9 @@ describe("OAuth2 credential provider picker", () => { test("bare get redirects to the picker", async () => { const core = coreWithProviders([providerItem({ name: "redirected" })]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get", { core }); + const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get", { + core, + }); await waitForText(screen.lastFrame, "redirected"); expect(core.identity.calls[0]?.method).toBe("listOauth2CredentialProviders"); @@ -152,7 +163,9 @@ describe("OAuth2 credential provider picker", () => { const name = "oauth2 blue"; const core = coreWithProviders([providerItem({ name })]); core.identity.setGetOauth2Response(getResponse({ name })); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { core }); + const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, name); await screen.press("return"); diff --git a/src/handlers/memory/actor/actor.screen.test.tsx b/src/handlers/memory/actor/actor.screen.test.tsx index 219130a951..8e87ffa896 100644 --- a/src/handlers/memory/actor/actor.screen.test.tsx +++ b/src/handlers/memory/actor/actor.screen.test.tsx @@ -3,7 +3,7 @@ import type { ActorSummary, SessionSummary } from "@aws-sdk/client-bedrock-agent import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/handlers/memory/event/event.screen.test.tsx b/src/handlers/memory/event/event.screen.test.tsx index 3797dd76bd..575b907d7b 100644 --- a/src/handlers/memory/event/event.screen.test.tsx +++ b/src/handlers/memory/event/event.screen.test.tsx @@ -3,7 +3,7 @@ import type { ActorSummary, Event, SessionSummary } from "@aws-sdk/client-bedroc import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/handlers/memory/memory.screen.test.tsx b/src/handlers/memory/memory.screen.test.tsx index aed4db7b3e..f38856eaf3 100644 --- a/src/handlers/memory/memory.screen.test.tsx +++ b/src/handlers/memory/memory.screen.test.tsx @@ -8,7 +8,7 @@ import { QueryClient } from "@tanstack/react-query"; import stringWidth from "string-width"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, tick, waitFor, diff --git a/src/handlers/memory/record/record.screen.test.tsx b/src/handlers/memory/record/record.screen.test.tsx index 8fea5d8660..527acac7a6 100644 --- a/src/handlers/memory/record/record.screen.test.tsx +++ b/src/handlers/memory/record/record.screen.test.tsx @@ -7,7 +7,7 @@ import type { import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/handlers/memory/session/session.screen.test.tsx b/src/handlers/memory/session/session.screen.test.tsx index e431689130..3cf3c334ac 100644 --- a/src/handlers/memory/session/session.screen.test.tsx +++ b/src/handlers/memory/session/session.screen.test.tsx @@ -1,6 +1,11 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { Event, SessionSummary } from "@aws-sdk/client-bedrock-agentcore"; -import { cleanupScreens, renderScreen, TestCoreClient, waitForText } from "../../../testing"; +import { + cleanupScreens, + renderImperativeScreen as renderScreen, + TestCoreClient, + waitForText, +} from "../../../testing"; afterEach(cleanupScreens); @@ -37,7 +42,9 @@ describe("Memory session list flow", () => { core.memory.setListEventsResponse({ events: [event({ memoryId, actorId, sessionId })], }); - const screen = renderScreen(`/agentcore/memory/session/list/${memoryId}/${actorId}`, { core }); + const screen = renderScreen(`/agentcore/memory/session/list/${memoryId}/${actorId}`, { + core, + }); await waitForText(screen.lastFrame, sessionId); const frame = screen.lastFrame()!; @@ -58,7 +65,9 @@ describe("Memory session list flow", () => { const core = new TestCoreClient(); core.memory.setError(new Error("sessions unavailable")); - const failed = renderScreen("/agentcore/memory/session/list/memory-1/actor-1", { core }); + const failed = renderScreen("/agentcore/memory/session/list/memory-1/actor-1", { + core, + }); await waitForText(failed.lastFrame, "sessions unavailable"); core.memory.setError(undefined); diff --git a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx index 05ee4c9c14..2aee43f723 100644 --- a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx +++ b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx @@ -7,7 +7,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/handlers/runtime/invoke/invoke.screen.test.tsx b/src/handlers/runtime/invoke/invoke.screen.test.tsx index a40b2b5df7..f6fba007fe 100644 --- a/src/handlers/runtime/invoke/invoke.screen.test.tsx +++ b/src/handlers/runtime/invoke/invoke.screen.test.tsx @@ -8,7 +8,7 @@ import type { import type { RuntimeInvokeRequest } from "../types"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, @@ -338,7 +338,9 @@ describe("Runtime invoke JSON console", () => { core.runtime.setGetResponse({ agentRuntimeArn: `arn:aws:bedrock-agentcore:${REGION}:123456789012:runtime/${runtimeId}`, } as GetAgentRuntimeResponse); - const screen = renderScreen(`/agentcore/runtime/invoke/${runtimeId}/${QUALIFIER}`, { core }); + const screen = renderScreen(`/agentcore/runtime/invoke/${runtimeId}/${QUALIFIER}`, { + core, + }); await waitForText(screen.lastFrame, "Ready"); await screen.resize(80, 24); diff --git a/src/handlers/runtime/runtime.screen.test.tsx b/src/handlers/runtime/runtime.screen.test.tsx index 0500d70c11..27db8213be 100644 --- a/src/handlers/runtime/runtime.screen.test.tsx +++ b/src/handlers/runtime/runtime.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, tick, waitFor, @@ -190,7 +190,9 @@ describe("runtime hub", () => { test("shows the Runtime failure reason only when the service provides one", async () => { const healthyCore = new TestCoreClient(); healthyCore.runtime.setGetResponse(getRuntimeResponse()); - const healthy = renderScreen("/agentcore/runtime/get/runtime-123", { core: healthyCore }); + const healthy = renderScreen("/agentcore/runtime/get/runtime-123", { + core: healthyCore, + }); await waitForText(healthy.lastFrame, "show the full JSON definition"); expect(healthy.lastFrame()).not.toContain("failureReason"); @@ -203,7 +205,9 @@ describe("runtime hub", () => { failureReason: "Image could not be pulled", }), ); - const failed = renderScreen("/agentcore/runtime/get/runtime-123", { core: failedCore }); + const failed = renderScreen("/agentcore/runtime/get/runtime-123", { + core: failedCore, + }); await waitForText(failed.lastFrame, "Image could not be pulled"); expect(failed.lastFrame()).toMatch(/failureReason\s+Image could not be pulled/); diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index 4989db37ac..be2432720f 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -5,7 +5,7 @@ import { ValueContext } from "../../../router"; import type { RuntimeShellSession } from "../types"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, tick, type TtyInput, diff --git a/src/handlers/runtime/version/version.screen.test.tsx b/src/handlers/runtime/version/version.screen.test.tsx index 53d801b68f..bb350b00d5 100644 --- a/src/handlers/runtime/version/version.screen.test.tsx +++ b/src/handlers/runtime/version/version.screen.test.tsx @@ -5,7 +5,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderScreen, + renderImperativeScreen as renderScreen, TestCoreClient, waitFor, waitForText, diff --git a/src/testing/globalConfig.tsx b/src/testing/globalConfig.tsx index 456c7164c2..c465aa3b5a 100644 --- a/src/testing/globalConfig.tsx +++ b/src/testing/globalConfig.tsx @@ -4,6 +4,11 @@ import { type GlobalConfigAccessor, } from "../globalConfig"; +export const IMPERATIVE_GLOBAL_CONFIG: GlobalConfig = { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-commands": true, +}; + type TestGlobalConfigAccessorOptions = { initialConfigData?: GlobalConfig; }; diff --git a/src/testing/index.tsx b/src/testing/index.tsx index 3ec0dbb448..ce915bf9ab 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -23,6 +23,7 @@ export { StreamController } from "./StreamController"; export { compiledRootCommand, renderScreen, + renderImperativeScreen, cleanupScreens, keys, waitForText, @@ -34,7 +35,7 @@ export { type RenderScreenResult, } from "./renderScreen"; export { createSilentLogger, assertLogsMatch, type LogQuery } from "./logging"; -export { TestGlobalConfigAccessor } from "./globalConfig"; +export { IMPERATIVE_GLOBAL_CONFIG, TestGlobalConfigAccessor } from "./globalConfig"; export { inTempDirectory, type TempDirectory } from "./fs"; export { initProject, type InitProjectOptions, type InitializedProject } from "./projects"; export { expectError } from "./errors"; diff --git a/src/testing/renderScreen.tsx b/src/testing/renderScreen.tsx index e99c823e11..1817757937 100644 --- a/src/testing/renderScreen.tsx +++ b/src/testing/renderScreen.tsx @@ -10,7 +10,7 @@ import { TestCoreClient } from "./TestCoreClient"; import { testIO } from "./testIO"; import { tick, waitFor } from "./timing"; import { createSilentLogger } from "./logging"; -import { TestGlobalConfigAccessor } from "./globalConfig"; +import { IMPERATIVE_GLOBAL_CONFIG, TestGlobalConfigAccessor } from "./globalConfig"; import { DEFAULT_GLOBAL_CONFIG, type GlobalConfig } from "../globalConfig"; // TUI test harness. @@ -183,6 +183,16 @@ export function renderScreen(path: string, options: RenderScreenOptions = {}): R }; } +export function renderImperativeScreen( + path: string, + options: RenderScreenOptions = {}, +): RenderScreenResult { + return renderScreen(path, { + ...options, + globalConfig: options.globalConfig ?? IMPERATIVE_GLOBAL_CONFIG, + }); +} + // waitForText resolves once `text` appears in the latest frame. export function waitForText( lastFrame: () => string | undefined, From 9e85cfc4cb189e0f31dc76a8456fb5089327f2da Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 23 Sep 2026 21:47:47 +0000 Subject: [PATCH 03/11] test(cli): explicitly enable standalone resource command fixtures --- src/handlers/gateway/gateway.create.test.tsx | 6 +++++- src/handlers/gateway/gateway.delete.test.tsx | 6 +++++- src/handlers/gateway/gateway.fixture.test.tsx | 6 +++++- src/handlers/gateway/gateway.mutations.test.tsx | 16 +++++++++++++--- src/handlers/gateway/gateway.policy.test.tsx | 6 +++++- src/handlers/gateway/gateway.test.tsx | 6 +++++- src/handlers/gateway/gateway.update.test.tsx | 6 +++++- src/handlers/gateway/invoke/invoke.test.tsx | 11 +++++++++-- src/handlers/harness/exec/exec.test.tsx | 13 +++++++++++-- src/handlers/harness/harness.test.tsx | 6 +++++- src/handlers/harness/invoke/invoke.test.tsx | 6 +++++- src/handlers/identity/identity.test.tsx | 11 +++++++++-- .../oauth2.fixture.test.tsx | 6 +++++- .../oauth2-credential-provider/oauth2.test.tsx | 11 +++++++++-- .../paymentCredentialProvider.read.test.tsx | 11 +++++++++-- src/handlers/memory/memory.test.tsx | 16 +++++++++++++--- .../payment/instrument/balance/balance.test.tsx | 12 ++++++++++-- src/handlers/payment/payment.read.test.tsx | 6 +++++- src/handlers/runtime/invoke/invoke.test.tsx | 6 +++++- src/handlers/runtime/logs/logs.test.tsx | 13 +++++++++++-- src/handlers/runtime/runtime.test.tsx | 16 +++++++++++++--- src/handlers/runtime/shell/shell.test.tsx | 6 +++++- src/handlers/runtime/traces/traces.test.tsx | 12 ++++++++++-- src/middleware/withRegion.test.tsx | 12 ++++++++++-- 24 files changed, 187 insertions(+), 39 deletions(-) diff --git a/src/handlers/gateway/gateway.create.test.tsx b/src/handlers/gateway/gateway.create.test.tsx index 6fa7d31648..04c0c16456 100644 --- a/src/handlers/gateway/gateway.create.test.tsx +++ b/src/handlers/gateway/gateway.create.test.tsx @@ -29,7 +29,11 @@ import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, +}; const REGION = "us-east-1"; const GATEWAY_NAME = "agentcore-cli-gateway-create-fixture"; const HTTP_TARGET_NAME = "http-fixture"; diff --git a/src/handlers/gateway/gateway.delete.test.tsx b/src/handlers/gateway/gateway.delete.test.tsx index 214d51ec62..19424a4a13 100644 --- a/src/handlers/gateway/gateway.delete.test.tsx +++ b/src/handlers/gateway/gateway.delete.test.tsx @@ -38,7 +38,11 @@ import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, +}; const REGION = "us-west-2"; const GATEWAY_ID = "gateway-1"; const TARGET_ID = "target-1"; diff --git a/src/handlers/gateway/gateway.fixture.test.tsx b/src/handlers/gateway/gateway.fixture.test.tsx index cb89916539..d845f73101 100644 --- a/src/handlers/gateway/gateway.fixture.test.tsx +++ b/src/handlers/gateway/gateway.fixture.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, fixtureFactories, matchGolden, @@ -47,9 +48,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[], region = REGION): Promise { const io = testIO(); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", region]); return io.stdout(); diff --git a/src/handlers/gateway/gateway.mutations.test.tsx b/src/handlers/gateway/gateway.mutations.test.tsx index 25510619e5..f9acab20d6 100644 --- a/src/handlers/gateway/gateway.mutations.test.tsx +++ b/src/handlers/gateway/gateway.mutations.test.tsx @@ -3,6 +3,7 @@ import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; import { compile, ValueContext } from "../../router"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, TestCoreClient, TestGlobalConfigAccessor, testIO, @@ -18,8 +19,12 @@ function setup(enabled?: boolean) { const io = testIO(); const globalConfig = enabled === undefined - ? undefined - : { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": enabled }; + ? IMPERATIVE_GLOBAL_CONFIG + : { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": enabled, + "imperative-commands": true, + }; const root = createRootHandler(core, { io: io.io, logger: createSilentLogger(), @@ -37,6 +42,7 @@ describe("Gateway imperative mutation availability", () => { const gateway = createGatewayHandler(new TestCoreClient(), testIO().io, { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": enabled, + "imperative-commands": true, }); const names = gateway.children().map((child) => child.name()); for (const mutation of MUTATIONS) { @@ -56,7 +62,11 @@ describe("Gateway imperative mutation availability", () => { io: testIO().io, logger: createSilentLogger(), globalConfigAccessor, - globalConfig: { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }, + globalConfig: { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, + }, }); const gateway = root.children().find((child) => child.name() === "gateway")!; expect(gateway.children().map((child) => child.name())).toContain("create"); diff --git a/src/handlers/gateway/gateway.policy.test.tsx b/src/handlers/gateway/gateway.policy.test.tsx index 3eb78044bd..3b993d5c9d 100644 --- a/src/handlers/gateway/gateway.policy.test.tsx +++ b/src/handlers/gateway/gateway.policy.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, fixtureFactories, matchGolden, @@ -58,9 +59,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[]): Promise<{ stdout: string; stderr: string }> { const io = testIO(); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route([ "node", diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index cb5e85a12f..6b3f50e6fa 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -22,7 +22,11 @@ import { createGeneratePolicyHandler } from "./policy/generate"; import type { Core } from "../types"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, +}; const REGION = "us-west-2"; const GATEWAY_ID = "gateway-1"; const TARGET_ID = "target-1"; diff --git a/src/handlers/gateway/gateway.update.test.tsx b/src/handlers/gateway/gateway.update.test.tsx index 56894ddca0..d4e857ebbd 100644 --- a/src/handlers/gateway/gateway.update.test.tsx +++ b/src/handlers/gateway/gateway.update.test.tsx @@ -34,7 +34,11 @@ import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, +}; async function runWithTestCore(args: string[]): Promise { const core = new TestCoreClient(); diff --git a/src/handlers/gateway/invoke/invoke.test.tsx b/src/handlers/gateway/invoke/invoke.test.tsx index 5080809540..3c87701597 100644 --- a/src/handlers/gateway/invoke/invoke.test.tsx +++ b/src/handlers/gateway/invoke/invoke.test.tsx @@ -9,6 +9,7 @@ import type { AppIO } from "../../../io"; import { ExitCode, InputValidationError, UserCancellationError } from "../../../errors"; import { runWithExitCode } from "../../../runnable"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, TestCoreClient, @@ -57,9 +58,12 @@ function captureIO(input?: Uint8Array) { async function runCommand(core: TestCoreClient, io: AppIO, args: string[]): Promise { const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); } @@ -577,9 +581,12 @@ describe("gateway invoke", () => { const core = configuredCore(); const output = captureIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: output.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const gateway = root.children().find((child) => child.name() === "gateway"); const invoke = gateway?.children().find((child) => child.name() === "invoke"); diff --git a/src/handlers/harness/exec/exec.test.tsx b/src/handlers/harness/exec/exec.test.tsx index 9435d969d8..801f9ee734 100644 --- a/src/handlers/harness/exec/exec.test.tsx +++ b/src/handlers/harness/exec/exec.test.tsx @@ -5,7 +5,13 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { createRootHandler } from "../../index"; -import { createSilentLogger, expectError, TestCoreClient, testIO } from "../../../testing"; +import { + IMPERATIVE_GLOBAL_CONFIG, + createSilentLogger, + expectError, + TestCoreClient, + testIO, +} from "../../../testing"; import { TestGlobalConfigAccessor } from "../../../testing/"; import { InputValidationError } from "../../../errors"; @@ -34,9 +40,12 @@ async function run(args: string[], configure?: (core: TestCoreClient) => void) { configure?.(core); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", "us-west-2"]); return { core, stdout: io.stdout() }; diff --git a/src/handlers/harness/harness.test.tsx b/src/handlers/harness/harness.test.tsx index eca5d1934d..3addcca635 100644 --- a/src/handlers/harness/harness.test.tsx +++ b/src/handlers/harness/harness.test.tsx @@ -3,6 +3,7 @@ import { join } from "node:path"; import { CoreClient } from "../../core"; import { createRootHandler } from "../index"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, fixtureFactories, @@ -55,9 +56,12 @@ async function run(args: string[]): Promise { const core = createFixtureCore(); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); return io.stdout(); diff --git a/src/handlers/harness/invoke/invoke.test.tsx b/src/handlers/harness/invoke/invoke.test.tsx index b1534b92ab..7b91fe8c8d 100644 --- a/src/handlers/harness/invoke/invoke.test.tsx +++ b/src/handlers/harness/invoke/invoke.test.tsx @@ -6,6 +6,7 @@ import type { import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { createRootHandler } from "../../index"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, TestCoreClient, @@ -51,9 +52,12 @@ async function run(args: string[], configure?: (core: TestCoreClient) => void, i core.harness.setInvokeEvents(...TURN_EVENTS); configure?.(core); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", "us-west-2"]); return { core, stdout: io.stdout() }; diff --git a/src/handlers/identity/identity.test.tsx b/src/handlers/identity/identity.test.tsx index 8266c2a7ec..7be8f12b73 100644 --- a/src/handlers/identity/identity.test.tsx +++ b/src/handlers/identity/identity.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, fixtureFactories, @@ -45,9 +46,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[], stdin?: string): Promise { const io = testIO({ stdin }); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -57,9 +61,12 @@ async function run(args: string[], stdin?: string): Promise { describe("identity command hierarchy", () => { test("registers the identity command hierarchy", () => { const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const identity = root.children().find((child) => child.name() === "identity"); diff --git a/src/handlers/identity/oauth2-credential-provider/oauth2.fixture.test.tsx b/src/handlers/identity/oauth2-credential-provider/oauth2.fixture.test.tsx index d19595d777..2940bef60c 100644 --- a/src/handlers/identity/oauth2-credential-provider/oauth2.fixture.test.tsx +++ b/src/handlers/identity/oauth2-credential-provider/oauth2.fixture.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, fixtureFactories, matchGolden, @@ -43,9 +44,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[], stdin?: string): Promise { const io = testIO({ stdin }); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); diff --git a/src/handlers/identity/oauth2-credential-provider/oauth2.test.tsx b/src/handlers/identity/oauth2-credential-provider/oauth2.test.tsx index 827c46caf8..96faf48d8e 100644 --- a/src/handlers/identity/oauth2-credential-provider/oauth2.test.tsx +++ b/src/handlers/identity/oauth2-credential-provider/oauth2.test.tsx @@ -6,6 +6,7 @@ import type { UpdateOauth2CredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, TestCoreClient, @@ -73,9 +74,12 @@ async function run( ): Promise<{ core: TestCoreClient; stdout: string }> { const io = testIO({ stdin }); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -85,9 +89,12 @@ async function run( describe("oauth2-credential-provider command hierarchy", () => { test("registers the oauth2-credential-provider command hierarchy", () => { const root = createRootHandler(new TestCoreClient(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const identity = root.children().find((child) => child.name() === "identity"); const oauth2 = identity diff --git a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx index 97678c000e..6d6d893197 100644 --- a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx +++ b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, fixtureFactories, @@ -41,9 +42,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[]): Promise { const io = testIO(); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...BASE, ...args, "--region", REGION]); @@ -53,9 +57,12 @@ async function run(args: string[]): Promise { describe("payment-credential-provider read-only command hierarchy", () => { test("registers get and list only, with no create, update, or delete commands", () => { const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const identity = root.children().find((child) => child.name() === "identity"); const payment = identity diff --git a/src/handlers/memory/memory.test.tsx b/src/handlers/memory/memory.test.tsx index f7c8c9f586..dd1916bd45 100644 --- a/src/handlers/memory/memory.test.tsx +++ b/src/handlers/memory/memory.test.tsx @@ -17,6 +17,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import { CoreClient } from "../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, fixtureFactories, @@ -96,9 +97,12 @@ function createFixtureCore(): CoreClient { function testMemoryCommand(core = new TestCoreClient()) { const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { @@ -111,9 +115,12 @@ function testMemoryCommand(core = new TestCoreClient()) { async function run(args: string[]): Promise { const io = testIO(); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -123,9 +130,12 @@ async function run(args: string[]): Promise { describe("memory command hierarchy", () => { test("registers the Memory read-only command hierarchy", () => { const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const memory = root.children().find((child) => child.name() === "memory"); const event = memory?.children().find((child) => child.name() === "event"); diff --git a/src/handlers/payment/instrument/balance/balance.test.tsx b/src/handlers/payment/instrument/balance/balance.test.tsx index ce33d12971..29da64e82a 100644 --- a/src/handlers/payment/instrument/balance/balance.test.tsx +++ b/src/handlers/payment/instrument/balance/balance.test.tsx @@ -7,7 +7,12 @@ import { import type { BedrockAgentCoreControlClient } from "@aws-sdk/client-bedrock-agentcore-control"; import { CoreClient } from "../../../../core"; import { createRootHandler } from "../../../index"; -import { createSilentLogger, TestGlobalConfigAccessor, testIO } from "../../../../testing"; +import { + IMPERATIVE_GLOBAL_CONFIG, + createSilentLogger, + TestGlobalConfigAccessor, + testIO, +} from "../../../../testing"; const ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/manager"; const scope = ["--manager-id", "manager", "--user-id", "user", "--instrument-id", "instrument"]; @@ -42,9 +47,12 @@ function setup(response: object = balance) { }); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { send, diff --git a/src/handlers/payment/payment.read.test.tsx b/src/handlers/payment/payment.read.test.tsx index 59a2017c9a..41fb9f2a20 100644 --- a/src/handlers/payment/payment.read.test.tsx +++ b/src/handlers/payment/payment.read.test.tsx @@ -3,6 +3,7 @@ import { join } from "node:path"; import { CoreClient } from "../../core"; import { compile, isTuiCommandSupported, ValueContext } from "../../router"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, fixtureFactories, matchGolden, @@ -29,9 +30,12 @@ function setup(resource = "manager", overrides: Partial { const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); } diff --git a/src/handlers/runtime/logs/logs.test.tsx b/src/handlers/runtime/logs/logs.test.tsx index 9bb09b92f1..1c25a86efa 100644 --- a/src/handlers/runtime/logs/logs.test.tsx +++ b/src/handlers/runtime/logs/logs.test.tsx @@ -2,7 +2,13 @@ import { describe, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createSilentLogger, expectError, TestCoreClient, testIO } from "../../../testing"; +import { + IMPERATIVE_GLOBAL_CONFIG, + createSilentLogger, + expectError, + TestCoreClient, + testIO, +} from "../../../testing"; import { TestGlobalConfigAccessor } from "../../../testing/globalConfig"; import { createRootHandler } from "../../index"; import type { LogSource } from "../../../core/observability/index"; @@ -16,9 +22,12 @@ function testLogsCommand() { const core = new TestCoreClient(); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 69324bd677..23c68ea1ab 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { CoreClient } from "../../core"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, fixtureFactories, @@ -69,9 +70,12 @@ function createFixtureCore(): CoreClient { async function run(args: string[]): Promise { const io = testIO(); const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -82,9 +86,12 @@ function testRuntimeCommand() { const core = new TestCoreClient(); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { @@ -96,9 +103,12 @@ function testRuntimeCommand() { describe("runtime command hierarchy", () => { test("registers the Runtime command hierarchy", () => { const root = createRootHandler(createFixtureCore(), { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); const runtime = root.children().find((child) => child.name() === "runtime"); diff --git a/src/handlers/runtime/shell/shell.test.tsx b/src/handlers/runtime/shell/shell.test.tsx index 68ab0a7fe5..730a2f10bd 100644 --- a/src/handlers/runtime/shell/shell.test.tsx +++ b/src/handlers/runtime/shell/shell.test.tsx @@ -3,6 +3,7 @@ import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore- import { createRootHandler } from "../../index"; import type { RuntimeShellRequest, RuntimeShellSession } from "../types"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, TestCoreClient, TestGlobalConfigAccessor, @@ -61,9 +62,12 @@ function harness(options: { isTTY?: boolean; runtime?: GetAgentRuntimeResponse } core.runtime.setShellSession(shell); const io = testIO({ isTTY: options.isTTY ?? true }); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { core, diff --git a/src/handlers/runtime/traces/traces.test.tsx b/src/handlers/runtime/traces/traces.test.tsx index 955b0562fe..50d390e6e4 100644 --- a/src/handlers/runtime/traces/traces.test.tsx +++ b/src/handlers/runtime/traces/traces.test.tsx @@ -3,7 +3,12 @@ import { mkdtempSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { createSilentLogger, TestCoreClient, testIO } from "../../../testing"; +import { + IMPERATIVE_GLOBAL_CONFIG, + createSilentLogger, + TestCoreClient, + testIO, +} from "../../../testing"; import { TestGlobalConfigAccessor } from "../../../testing/globalConfig"; import { createRootHandler } from "../../index"; import type { GetTraceQuery, ListTracesQuery } from "../../../core/observability/index"; @@ -18,9 +23,12 @@ function testTracesCommand() { const core = new TestCoreClient(); const io = testIO(); const root = createRootHandler(core, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), }); return { diff --git a/src/middleware/withRegion.test.tsx b/src/middleware/withRegion.test.tsx index 1404f93ac8..fe31c930b6 100644 --- a/src/middleware/withRegion.test.tsx +++ b/src/middleware/withRegion.test.tsx @@ -3,7 +3,12 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createRootHandler } from "../handlers"; -import { TestCoreClient, TestGlobalConfigAccessor, testIO } from "../testing"; +import { + IMPERATIVE_GLOBAL_CONFIG, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../testing"; import { createSilentLogger } from "../testing/"; // writeConfigFile writes an AWS shared-config file with the given contents to a @@ -27,7 +32,10 @@ async function resolvedRegion(args: string[]): Promise { const root = createRootHandler(core, { io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", "harness", "list", "--json", ...args]); const call = core.harness.calls.at(-1); From 3eaf99d28397d88ae792f6fbcc3f0c7152928fb1 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 23 Sep 2026 21:49:30 +0000 Subject: [PATCH 04/11] feat(cli): gate standalone resource families at the root --- scripts/generate-command-reference.mjs | 35 ++-- src/components/CliOnlyScreen.test.tsx | 15 +- src/components/CliOnlyScreen.tsx | 9 +- src/components/RouterScreen.test.tsx | 46 ++--- src/components/RouterScreen.tsx | 25 ++- src/handlers/help.screen.test.tsx | 4 +- src/handlers/imperative.test.tsx | 184 ++++++++++++++++++ src/handlers/index.tsx | 14 +- .../project/status/status.screen.test.tsx | 11 +- src/handlers/root.test.tsx | 6 - src/tui/tui.test.tsx | 17 +- 11 files changed, 290 insertions(+), 76 deletions(-) create mode 100644 src/handlers/imperative.test.tsx diff --git a/scripts/generate-command-reference.mjs b/scripts/generate-command-reference.mjs index 106f16f89a..068cbab281 100644 --- a/scripts/generate-command-reference.mjs +++ b/scripts/generate-command-reference.mjs @@ -10,12 +10,12 @@ const REPOSITORY_ROOT = resolve(SCRIPT_DIR, ".."); const DEFAULT_GROUPS = [ { id: "global-options", title: "Global options", commands: [] }, { id: "project", title: "Project commands", commands: ["project"] }, - { id: "harness", title: "Harness commands", commands: ["harness"] }, - { id: "identity", title: "Identity commands", commands: ["identity"] }, - { id: "runtime", title: "Runtime commands", commands: ["runtime"] }, - { id: "memory", title: "Memory commands", commands: ["memory"] }, - { id: "gateway", title: "Gateway commands", commands: ["gateway"] }, - { id: "payment", title: "Payment commands", commands: ["payment"] }, + { id: "harness", title: "Harness commands", commands: ["harness"], optional: true }, + { id: "identity", title: "Identity commands", commands: ["identity"], optional: true }, + { id: "runtime", title: "Runtime commands", commands: ["runtime"], optional: true }, + { id: "memory", title: "Memory commands", commands: ["memory"], optional: true }, + { id: "gateway", title: "Gateway commands", commands: ["gateway"], optional: true }, + { id: "payment", title: "Payment commands", commands: ["payment"], optional: true }, { id: "evaluation", title: "Evaluation commands", commands: ["eval"] }, { id: "settings", @@ -182,7 +182,10 @@ function buildModel({ version, groups = DEFAULT_GROUPS, env }) { const discovered = new Set(rootEntry.members.map((entry) => entry.name.split(" ").at(-1))); const grouped = new Set(groups.flatMap((group) => group.commands)); const missing = [...discovered].filter((command) => !grouped.has(command)); - const unknown = [...grouped].filter((command) => !discovered.has(command)); + const unknown = groups + .filter((group) => !group.optional) + .flatMap((group) => group.commands) + .filter((command) => !discovered.has(command)); if (missing.length) { throw new Error(`Top-level commands missing from groups: ${missing.join(", ")}`); @@ -197,13 +200,17 @@ function buildModel({ version, groups = DEFAULT_GROUPS, env }) { return { version, - groups: groups.map((group) => ({ - title: group.title, - entries: - group.id === "global-options" - ? [{ ...rootEntry, members: [] }] - : group.commands.map((command) => entriesByCommand.get(command)), - })), + groups: groups + .filter( + (group) => !group.optional || group.commands.some((command) => discovered.has(command)), + ) + .map((group) => ({ + title: group.title, + entries: + group.id === "global-options" + ? [{ ...rootEntry, members: [] }] + : group.commands.map((command) => entriesByCommand.get(command)), + })), }; } diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index 16e483b1da..27a00db537 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -5,14 +5,15 @@ import { compiledRootCommand, menuEntries, renderScreen, + renderImperativeScreen, + IMPERATIVE_GLOBAL_CONFIG, waitForText, } from "../testing"; import { isTuiCommandSupported } from "../router"; -import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; afterEach(cleanupScreens); -const MUTATION_CONFIG = { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true }; +const MUTATION_CONFIG = { ...IMPERATIVE_GLOBAL_CONFIG, "imperative-mutation-commands": true }; // cliOnlyCommands walks the compiled Commander tree for every command without // a screen, so a command added later is covered without a new test. `help` is @@ -39,8 +40,8 @@ describe("menus list command-line-only subcommands below a divider", () => { await waitForText(r.lastFrame, "command line only"); expect(menuEntries(r.lastFrame()!)).toEqual({ - screens: ["project", "harness", "identity", "runtime", "memory", "gateway", "eval"], - cliOnly: ["payment", "feedback", "config", "update"], + screens: ["project", "eval"], + cliOnly: ["feedback", "config", "update"], }); r.unmount(); }); @@ -65,7 +66,7 @@ describe("menus list command-line-only subcommands below a divider", () => { }); test("the harness menu", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "command line only"); expect(menuEntries(r.lastFrame()!)).toEqual({ @@ -86,7 +87,7 @@ describe("menus list command-line-only subcommands below a divider", () => { }); test("the divider is omitted when nothing is command line only", async () => { - const r = renderScreen("/agentcore/harness/endpoint"); + const r = renderImperativeScreen("/agentcore/harness/endpoint"); await waitForText(r.lastFrame, "manage harness endpoints"); expect(r.lastFrame()).not.toContain("command line only"); @@ -141,7 +142,7 @@ describe("paths without a screen of their own", () => { await waitForText(() => r.frames.join("\n"), "Usage:"); const output = r.frames.join("\n"); - expect(output).toContain("harness"); + expect(output).toContain("project"); expect(output).not.toContain("command line only"); r.unmount(); }); diff --git a/src/components/CliOnlyScreen.tsx b/src/components/CliOnlyScreen.tsx index 350c955d55..dc04c7b273 100644 --- a/src/components/CliOnlyScreen.tsx +++ b/src/components/CliOnlyScreen.tsx @@ -1,13 +1,12 @@ import { useRef, type ReactNode } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; -import type { Command } from "commander"; import { useLocation, useNavigate } from "react-router"; import { CommandKey, commandParameterDetails } from "../router"; import type { ScreenProps } from "../handlers/types"; import { Layout } from "./Layout"; import { KeyValueTable } from "./KeyValueTable"; -import { RouterScreen, resolveCommand } from "./RouterScreen"; +import { RouterScreen, commandPath, resolveCommand } from "./RouterScreen"; import { darkTheme } from "./ui/_core.js"; const theme = darkTheme; @@ -172,9 +171,3 @@ export function CommandFallbackScreen({ ); } - -function commandPath(command: Command): string[] { - const names: string[] = []; - for (let cur: Command | null = command; cur; cur = cur.parent) names.unshift(cur.name()); - return names; -} diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 1ccd88beb7..ae5f39a518 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -1,5 +1,11 @@ import { test, expect, describe, afterEach } from "bun:test"; -import { cleanupScreens, renderScreen, tick, waitForText } from "../testing"; +import { + cleanupScreens, + renderScreen, + renderImperativeScreen, + tick, + waitForText, +} from "../testing"; afterEach(cleanupScreens); @@ -10,17 +16,14 @@ afterEach(cleanupScreens); describe("menu rendering", () => { test("lists the current command's subcommands with their descriptions", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "harness"); + await waitForText(r.lastFrame, "type to choose a command"); const frame = r.lastFrame()!; - expect(frame).toContain("harness"); - expect(frame).toContain("manage AgentCore harnesses"); - expect(frame).toContain("runtime"); - expect(frame).toContain("inspect AgentCore Runtimes"); - expect(frame).toContain("memory"); - expect(frame).toContain("inspect AgentCore Memories"); - expect(frame).toContain("gateway"); - expect(frame).toContain("manage AgentCore Gateways"); + expect(frame).toContain("project"); + expect(frame).toContain("eval"); + for (const family of ["harness", "identity", "runtime", "memory", "gateway", "payment"]) { + expect(frame).not.toMatch(new RegExp(`\\b${family}\\b`)); + } expect(frame).toContain("config"); expect(frame).toContain("read/write global config values"); r.unmount(); @@ -33,7 +36,7 @@ describe("menu rendering", () => { }); test("renders the harness subcommands when mounted at the harness path", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); const frame = r.lastFrame()!; @@ -45,7 +48,7 @@ describe("menu rendering", () => { test("highlights the first option by default", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "harness"); + await waitForText(r.lastFrame, "type to choose a command"); // The focus caret marks the highlighted row; the first option is project. expect(r.lastFrame()).toContain("❯ project"); r.unmount(); @@ -54,7 +57,7 @@ describe("menu rendering", () => { describe("filtering", () => { test("typing narrows the options to matches", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); await r.write("cr"); // matches "create" only @@ -68,7 +71,7 @@ describe("filtering", () => { }); test("filtering is case-insensitive", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); await r.write("LIST"); @@ -77,7 +80,7 @@ describe("filtering", () => { }); test("shows a no-matches message when nothing matches", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); await r.write("zzz"); @@ -92,10 +95,10 @@ describe("navigation", () => { await waitForText(r.lastFrame, "❯ project"); await r.press("down"); - await waitForText(r.lastFrame, "❯ harness"); + await waitForText(r.lastFrame, "❯ eval"); await r.press("down"); - await waitForText(r.lastFrame, "❯ identity"); + await waitForText(r.lastFrame, "❯ feedback"); r.unmount(); }); @@ -115,16 +118,15 @@ describe("navigation", () => { await waitForText(r.lastFrame, "❯ project"); await r.press("down"); - await waitForText(r.lastFrame, "❯ harness"); + await waitForText(r.lastFrame, "❯ eval"); await r.press("return"); - // The harness screen is itself a RouterScreen showing harness subcommands. - await waitForText(r.lastFrame, "agentcore → harness"); - expect(r.lastFrame()).toContain("list"); + await waitForText(r.lastFrame, "agentcore → eval"); + expect(r.lastFrame()).toContain("evaluator"); r.unmount(); }); test("esc from a nested menu returns to the parent menu", async () => { - const r = renderScreen("/agentcore/harness"); + const r = renderImperativeScreen("/agentcore/harness"); await waitForText(r.lastFrame, "agentcore → harness"); await r.press("escape"); diff --git a/src/components/RouterScreen.tsx b/src/components/RouterScreen.tsx index 3f98062f95..5c131d9107 100644 --- a/src/components/RouterScreen.tsx +++ b/src/components/RouterScreen.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import { Box, Text, useApp, useInput, useStdin } from "ink"; import type { Command } from "commander"; -import { useNavigate } from "react-router"; +import { Navigate, useNavigate } from "react-router"; import { CommandKey, isTuiCommandSupported } from "../router"; import { Layout } from "./Layout"; import { Divider } from "./ui/divider"; @@ -35,6 +35,12 @@ export function resolveCommand(launch: Command, path: string[]): Command { return cur; } +export function commandPath(command: Command): string[] { + const names: string[] = []; + for (let cur: Command | null = command; cur; cur = cur.parent) names.unshift(cur.name()); + return names; +} + interface Option { name: string; description: string; @@ -63,7 +69,21 @@ export interface RouterScreenProps extends ScreenProps { // Command) as navigable options below. Selecting an option routes to that // subcommand's screen. Subcommands without a screen are listed below a divider // and open their help instead (see CliOnlyScreen). -export function RouterScreen({ ctx, path, tuiOnlyCommands = [] }: RouterScreenProps) { +export function RouterScreen(props: RouterScreenProps) { + const command = resolveCommand(props.ctx.require(CommandKey), props.path); + const resolvedPath = commandPath(command); + // Project views reuse resource screens, but disabled command menus do not exist. + if (resolvedPath.join("/") !== props.path.join("/")) { + return ; + } + return ; +} + +function CommandMenu({ + path, + tuiOnlyCommands = [], + command, +}: RouterScreenProps & { command: Command }) { const navigate = useNavigate(); const { isRawModeSupported } = useStdin(); const { exit } = useApp(); @@ -75,7 +95,6 @@ export function RouterScreen({ ctx, path, tuiOnlyCommands = [] }: RouterScreenPr pinRegion(undefined); }, [pinRegion]); - const command = resolveCommand(ctx.require(CommandKey), path); // Screen-backed commands first, then the command-line-only ones, so the // divider between them falls at one place in the list. const options: Option[] = useMemo(() => { diff --git a/src/handlers/help.screen.test.tsx b/src/handlers/help.screen.test.tsx index 400dce8822..97a9494bc7 100644 --- a/src/handlers/help.screen.test.tsx +++ b/src/handlers/help.screen.test.tsx @@ -27,7 +27,9 @@ describe("HelpScreen", () => { const output = frames.join("\n"); expect(output).toContain("Usage:"); - expect(output).toContain("harness"); + expect(output).toContain("project"); + expect(output).toContain("eval"); + expect(output).not.toContain("harness"); expect(output).toContain("config"); }); }); diff --git a/src/handlers/imperative.test.tsx b/src/handlers/imperative.test.tsx new file mode 100644 index 0000000000..a844a24b75 --- /dev/null +++ b/src/handlers/imperative.test.tsx @@ -0,0 +1,184 @@ +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; +import { compile, ValueContext } from "../router"; +import { + cleanupScreens, + createSilentLogger, + menuEntries, + renderScreen, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, + waitForText, +} from "../testing"; +import { createRootHandler } from "./index"; + +afterEach(() => { + cleanupScreens(); + mock.restore(); +}); + +const FAMILIES = ["harness", "identity", "runtime", "memory", "gateway", "payment"] as const; +const PUBLIC_COMMANDS = ["project", "eval", "feedback", "config", "update"]; +const ALL_COMMANDS = ["project", ...FAMILIES, "eval", "feedback", "config", "update"]; +const STATES = [undefined, false, true].flatMap((enabled) => + [false, true].map((mutations) => ({ enabled, mutations })), +); + +function configFor(enabled: boolean | undefined, mutations: boolean) { + return { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": mutations, + ...(enabled === undefined ? {} : { "imperative-commands": enabled }), + }; +} + +function setup(enabled: boolean | undefined, mutations: boolean) { + const core = new TestCoreClient(); + const paymentCalls = [ + spyOn(core.payment, "getPaymentManager"), + spyOn(core.payment, "listPaymentManagers"), + spyOn(core.payment, "getPaymentConnector"), + spyOn(core.payment, "listPaymentConnectors"), + spyOn(core.payment, "getPaymentSession"), + spyOn(core.payment, "listPaymentSessions"), + spyOn(core.payment, "getPaymentInstrument"), + spyOn(core.payment, "getPaymentInstrumentBalance"), + spyOn(core.payment, "listPaymentInstruments"), + ]; + const globalConfig = configFor(enabled, mutations); + const root = createRootHandler(core, { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: globalConfig }), + globalConfig, + }); + const command = compile(root, ValueContext.EmptyContext()); + command.configureOutput({ writeErr: () => {}, writeOut: () => {} }); + const expectNoCoreCalls = () => { + for (const family of FAMILIES) { + if (family !== "payment") expect(core[family].calls).toEqual([]); + } + for (const call of paymentCalls) expect(call).not.toHaveBeenCalled(); + }; + return { core, command, expectNoCoreCalls }; +} + +describe("imperative command families", () => { + test.each(STATES)( + "root flag $enabled and Gateway mutation flag $mutations select the command tree", + ({ enabled, mutations }) => { + const { command } = setup(enabled, mutations); + expect(command.commands.map((child) => child.name())).toEqual( + enabled ? ALL_COMMANDS : PUBLIC_COMMANDS, + ); + for (const family of FAMILIES) { + expect(new RegExp(`\\n\\s+${family}\\s`).test(command.helpInformation())).toBe( + enabled === true, + ); + } + if (enabled) { + const gateway = command.commands.find((child) => child.name() === "gateway")!; + const groups = [ + gateway, + ...gateway.commands.filter((child) => + ["target", "connector", "rule"].includes(child.name()), + ), + ]; + for (const group of groups) { + for (const mutation of ["create", "update", "delete"]) { + expect(group.commands.some((child) => child.name() === mutation)).toBe(mutations); + } + } + } + const project = command.commands.find((child) => child.name() === "project")!; + expect(project.commands.map((child) => child.name())).toContain("invoke"); + expect( + project.commands + .find((child) => child.name() === "add") + ?.commands.map((child) => child.name()), + ).toEqual(expect.arrayContaining(["harness", "runtime", "memory", "gateway"])); + const evaluation = command.commands.find((child) => child.name() === "eval")!; + expect(evaluation.commands.map((child) => child.name())).toContain("evaluator"); + }, + ); + + test.each([...FAMILIES])( + "disabled %s rejects CLI requests without calling Core", + async (family) => { + for (const mutations of [false, true]) { + for (const args of [[], ["list", "--json"], ["get", "--id", "disabled"]]) { + const { command, expectNoCoreCalls } = setup(false, mutations); + await expect( + command.parseAsync(["node", "agentcore", family, ...args]), + ).rejects.toThrow(); + expectNoCoreCalls(); + } + } + }, + ); + + test("root registration uses the resolved snapshot without reading config", () => { + const accessor = new TestGlobalConfigAccessor(); + let reads = 0; + accessor.get = async () => { + reads++; + throw new Error("Must not read config during construction"); + }; + const root = createRootHandler(new TestCoreClient(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: accessor, + globalConfig: configFor(false, true), + }); + expect(root.children().map((child) => child.name())).toEqual(PUBLIC_COMMANDS); + expect(reads).toBe(0); + }); +}); + +describe("imperative command menus", () => { + test.each(STATES)( + "root menu matches parent $enabled and Gateway mutation $mutations", + async ({ enabled, mutations }) => { + const { core, expectNoCoreCalls } = setup(enabled, mutations); + const screen = renderScreen("/agentcore", { + core, + globalConfig: configFor(enabled, mutations), + }); + await waitForText(screen.lastFrame, "type to choose a command"); + expect(menuEntries(screen.lastFrame()!)).toEqual({ + screens: enabled + ? ["project", "harness", "identity", "runtime", "memory", "gateway", "eval"] + : ["project", "eval"], + cliOnly: enabled + ? ["payment", "feedback", "config", "update"] + : ["feedback", "config", "update"], + }); + expectNoCoreCalls(); + }, + ); + + test.each([ + ...FAMILIES.filter((family) => family !== "payment"), + "runtime/endpoint", + "harness/version", + "gateway/target", + ])("unavailable menu %s returns to a registered menu", async (path) => { + const { core, expectNoCoreCalls } = setup(false, false); + const screen = renderScreen(`/agentcore/${path}`, { core }); + await waitForText(screen.lastFrame, "the platform for production AI agents"); + expect(menuEntries(screen.lastFrame()!)).toEqual({ + screens: ["project", "eval"], + cliOnly: ["feedback", "config", "update"], + }); + expectNoCoreCalls(); + }); + + test("an unavailable CLI-only family retains the unknown-route fallback", async () => { + const { core, expectNoCoreCalls } = setup(false, false); + const screen = renderScreen("/agentcore/payment", { core }); + await waitForText(() => screen.frames.join("\n"), "Usage:"); + expect(screen.frames.join("\n")).not.toContain("payment"); + expectNoCoreCalls(); + }); +}); diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index c6687c9d12..84d8b233f2 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -77,12 +77,14 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router // Install sub handlers. Registration order is menu/help order; project is // the primary workflow, so it goes first. root.handler(createProjectHandler({ core, io })); - root.handler(createHarnessHandler(core, io)); - root.handler(createIdentityHandler(core, io)); - root.handler(createRuntimeHandler(core, io)); - root.handler(createMemoryHandler(core, io)); - root.handler(createGatewayHandler(core, io, globalConfig)); - root.handler(createPaymentHandler(core, io)); + if (globalConfig["imperative-commands"]) { + root.handler(createHarnessHandler(core, io)); + root.handler(createIdentityHandler(core, io)); + root.handler(createRuntimeHandler(core, io)); + root.handler(createMemoryHandler(core, io)); + root.handler(createGatewayHandler(core, io, globalConfig)); + root.handler(createPaymentHandler(core, io)); + } root.handler(createEvalHandler(core, io)); root.handler(createFeedbackHandler(core, io)); root.handler(createConfigHandler()); diff --git a/src/handlers/project/status/status.screen.test.tsx b/src/handlers/project/status/status.screen.test.tsx index 0c4d63d89a..e2c86f5c5f 100644 --- a/src/handlers/project/status/status.screen.test.tsx +++ b/src/handlers/project/status/status.screen.test.tsx @@ -343,13 +343,16 @@ describe("project status screen", () => { // launch region again. await screen.press("escape"); await waitForText(screen.lastFrame, "❯ project"); - await screen.write("harness"); + await screen.write("eval"); await screen.press("return"); - await waitForText(screen.lastFrame, "agentcore → harness"); + await waitForText(screen.lastFrame, "agentcore → eval"); + await screen.write("evaluator"); + await screen.press("return"); + await waitForText(screen.lastFrame, "agentcore → eval → evaluator"); await screen.write("list"); await screen.press("return"); - await waitFor(() => value.harness.calls.some(({ method }) => method === "listHarnesses")); - const listCall = value.harness.calls.find(({ method }) => method === "listHarnesses")!; + await waitFor(() => value.eval.calls.some(({ method }) => method === "listEvaluators")); + const listCall = value.eval.calls.find(({ method }) => method === "listEvaluators")!; expect(listCall.args[2]).toMatchObject({ region: "us-east-1" }); }); diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index 2a128f467d..b94c57b3fb 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -12,12 +12,6 @@ describe("createRootHandler", () => { expect(root.name()).toBe("agentcore"); expect(root.children().map((c) => c.name())).toEqual([ "project", - "harness", - "identity", - "runtime", - "memory", - "gateway", - "payment", "eval", "feedback", "config", diff --git a/src/tui/tui.test.tsx b/src/tui/tui.test.tsx index 47762ab56e..388a7b78ae 100644 --- a/src/tui/tui.test.tsx +++ b/src/tui/tui.test.tsx @@ -2,8 +2,10 @@ import { test, expect, describe } from "bun:test"; import { createRootHandler } from "../handlers"; import { ExitCode, InvalidEnvironmentError } from "../errors"; import { renderJson } from "./index"; +import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, TestCoreClient, TestGlobalConfigAccessor, testIO, @@ -25,12 +27,13 @@ describe("--json short-circuits the TUI", () => { // instead of launching the interactive TUI (renderTui's JSON branch). This // keeps the CLI scriptable and, importantly, keeps these tests from trying to // mount Ink against a non-TTY stdin. - async function runRoot(args: string[]): Promise { + async function runRoot(args: string[], globalConfig = DEFAULT_GLOBAL_CONFIG): Promise { const io = testIO(); const root = createRootHandler(new TestCoreClient(), { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: globalConfig }), + globalConfig, }); await root.route(["node", "agentcore", ...args, "--json"]); return io.stdout(); @@ -39,11 +42,12 @@ describe("--json short-circuits the TUI", () => { test("bare `agentcore --json` prints help rather than opening the TUI", async () => { const out = await runRoot([]); expect(out).toContain("Usage:"); - expect(out).toContain("harness"); + expect(out).toContain("project"); + expect(out).not.toContain("harness"); }); test("`agentcore harness --json` prints the harness command's help", async () => { - const out = await runRoot(["harness"]); + const out = await runRoot(["harness"], IMPERATIVE_GLOBAL_CONFIG); expect(out).toContain("Usage:"); // The harness subcommands are listed in its help. expect(out).toContain("list"); @@ -94,7 +98,10 @@ describe("TUI stream boundary", () => { const root = createRootHandler(core, { io: streams.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); const routePromise = root.route(["node", "agentcore", "runtime", "list"]); const listCalls = () => core.runtime.calls.filter((call) => call.method === "listRuntimes"); From e3aaa8a25c53613b540f4848b69fc162598d2374 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 23 Sep 2026 21:50:42 +0000 Subject: [PATCH 05/11] docs: present project and evaluation workflows by default --- CONTRIBUTING.md | 2 +- README.md | 32 +- command.md | 1350 ----------------------------------------------- 3 files changed, 14 insertions(+), 1370 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c316f5086..d43b312701 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,7 +195,7 @@ command — branch or leaf — is a `Handler`: ("global") flags and middleware that apply to everything beneath them. A branch can also register a **default handler** (`router.default(...)`) that runs when the branch is invoked with no subcommand (e.g. bare `agentcore` or - `agentcore harness` — this is how the TUI launches). + `agentcore project` — this is how the TUI launches). - **Leaf nodes** (built with `createHandler(...)`) do the work. They declare their own flags/arguments (validated and coerced via zod schemas) and receive a typed object in `handle(ctx, flags, args)`. diff --git a/README.md b/README.md index 50f13231e0..e462f30244 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It gives you two ways to work, from the same package: ```bash agentcore # launch the interactive TUI -agentcore harness list --json # scriptable, machine-readable output +agentcore project status --json # scriptable, machine-readable output ``` ## What problem does it solve? @@ -45,33 +45,27 @@ agentcore project create --name MyAgent --template agent-python-strands ## Command Surface `project` commands manage local project specifications and their deployments. -Resource commands operate on deployed resources without requiring a local project. - -| Command | Purpose | -| ---------- | ----------------------------------------------------------------------------- | -| `project` | Create, develop, build, deploy, invoke, and inspect a project | -| `harness` | Manage Harnesses, versions, and endpoints; invoke and inspect them | -| `identity` | Manage credential providers | -| `runtime` | Inspect, invoke, and open a shell in deployed Runtimes | -| `memory` | Inspect Memories, actors, sessions, events, and records | -| `gateway` | Inspect and invoke Gateways, inspect targets and rules, and generate policies | -| `payment` | Inspect payment managers, connectors, sessions, instruments, and balances | -| `eval` | Evaluate agents, manage datasets and configurations, and run experiments | -| `feedback` | Submit feedback | -| `config` | Read and write global CLI settings | -| `update` | Check for and install CLI updates | +`eval` commands evaluate deployed resources without requiring a local project. + +| Command | Purpose | +| ---------- | ------------------------------------------------------------------------ | +| `project` | Create, develop, build, deploy, invoke, and inspect a project | +| `eval` | Evaluate agents, manage datasets and configurations, and run experiments | +| `feedback` | Submit feedback | +| `config` | Read and write global CLI settings | +| `update` | Check for and install CLI updates | Use `--help` for subcommands and flags, or browse the [command reference](command.md): ```bash agentcore --help agentcore project --help -agentcore runtime invoke --help +agentcore project invoke --help ``` Supported bare commands open their interactive flows in a terminal. Operation -flags select headless behavior for most commands, but invoke commands can use -selectors such as `--id` and `--session-id` to seed an interactive console. +flags select headless behavior for most commands. Project invoke commands can +use `--name` and `--target` to select a deployed agent for an interactive session. Run `agentcore project create` for guided setup. To create a default project without the wizard, run `agentcore project create --name MyAssistant`. diff --git a/command.md b/command.md index 8ce9f3f124..80d44c9873 100644 --- a/command.md +++ b/command.md @@ -53,106 +53,6 @@ This reference was generated from `agentcore --help` for version `1.0.0-rc.4`. - [`agentcore project traces harness get`](#agentcore-project-traces-harness-get) - [`agentcore project status`](#agentcore-project-status) - [`agentcore project build`](#agentcore-project-build) -- [Harness commands](#harness-commands) - - [`agentcore harness`](#agentcore-harness) - - [`agentcore harness create`](#agentcore-harness-create) - - [`agentcore harness get`](#agentcore-harness-get) - - [`agentcore harness list`](#agentcore-harness-list) - - [`agentcore harness update`](#agentcore-harness-update) - - [`agentcore harness delete`](#agentcore-harness-delete) - - [`agentcore harness invoke`](#agentcore-harness-invoke) - - [`agentcore harness exec`](#agentcore-harness-exec) - - [`agentcore harness logs`](#agentcore-harness-logs) - - [`agentcore harness traces`](#agentcore-harness-traces) - - [`agentcore harness traces list`](#agentcore-harness-traces-list) - - [`agentcore harness traces get`](#agentcore-harness-traces-get) - - [`agentcore harness endpoint`](#agentcore-harness-endpoint) - - [`agentcore harness endpoint create`](#agentcore-harness-endpoint-create) - - [`agentcore harness endpoint get`](#agentcore-harness-endpoint-get) - - [`agentcore harness endpoint list`](#agentcore-harness-endpoint-list) - - [`agentcore harness endpoint update`](#agentcore-harness-endpoint-update) - - [`agentcore harness endpoint delete`](#agentcore-harness-endpoint-delete) - - [`agentcore harness version`](#agentcore-harness-version) - - [`agentcore harness version get`](#agentcore-harness-version-get) - - [`agentcore harness version list`](#agentcore-harness-version-list) -- [Identity commands](#identity-commands) - - [`agentcore identity`](#agentcore-identity) - - [`agentcore identity api-key-credential-provider`](#agentcore-identity-api-key-credential-provider) - - [`agentcore identity api-key-credential-provider create`](#agentcore-identity-api-key-credential-provider-create) - - [`agentcore identity api-key-credential-provider get`](#agentcore-identity-api-key-credential-provider-get) - - [`agentcore identity api-key-credential-provider list`](#agentcore-identity-api-key-credential-provider-list) - - [`agentcore identity api-key-credential-provider update`](#agentcore-identity-api-key-credential-provider-update) - - [`agentcore identity api-key-credential-provider delete`](#agentcore-identity-api-key-credential-provider-delete) - - [`agentcore identity oauth2-credential-provider`](#agentcore-identity-oauth2-credential-provider) - - [`agentcore identity oauth2-credential-provider create`](#agentcore-identity-oauth2-credential-provider-create) - - [`agentcore identity oauth2-credential-provider get`](#agentcore-identity-oauth2-credential-provider-get) - - [`agentcore identity oauth2-credential-provider list`](#agentcore-identity-oauth2-credential-provider-list) - - [`agentcore identity oauth2-credential-provider update`](#agentcore-identity-oauth2-credential-provider-update) - - [`agentcore identity oauth2-credential-provider delete`](#agentcore-identity-oauth2-credential-provider-delete) - - [`agentcore identity payment-credential-provider`](#agentcore-identity-payment-credential-provider) - - [`agentcore identity payment-credential-provider get`](#agentcore-identity-payment-credential-provider-get) - - [`agentcore identity payment-credential-provider list`](#agentcore-identity-payment-credential-provider-list) -- [Runtime commands](#runtime-commands) - - [`agentcore runtime`](#agentcore-runtime) - - [`agentcore runtime get`](#agentcore-runtime-get) - - [`agentcore runtime list`](#agentcore-runtime-list) - - [`agentcore runtime invoke`](#agentcore-runtime-invoke) - - [`agentcore runtime shell`](#agentcore-runtime-shell) - - [`agentcore runtime version`](#agentcore-runtime-version) - - [`agentcore runtime version get`](#agentcore-runtime-version-get) - - [`agentcore runtime version list`](#agentcore-runtime-version-list) - - [`agentcore runtime endpoint`](#agentcore-runtime-endpoint) - - [`agentcore runtime endpoint get`](#agentcore-runtime-endpoint-get) - - [`agentcore runtime endpoint list`](#agentcore-runtime-endpoint-list) - - [`agentcore runtime logs`](#agentcore-runtime-logs) - - [`agentcore runtime traces`](#agentcore-runtime-traces) - - [`agentcore runtime traces list`](#agentcore-runtime-traces-list) - - [`agentcore runtime traces get`](#agentcore-runtime-traces-get) -- [Memory commands](#memory-commands) - - [`agentcore memory`](#agentcore-memory) - - [`agentcore memory get`](#agentcore-memory-get) - - [`agentcore memory list`](#agentcore-memory-list) - - [`agentcore memory event`](#agentcore-memory-event) - - [`agentcore memory event get`](#agentcore-memory-event-get) - - [`agentcore memory event list`](#agentcore-memory-event-list) - - [`agentcore memory record`](#agentcore-memory-record) - - [`agentcore memory record get`](#agentcore-memory-record-get) - - [`agentcore memory record list`](#agentcore-memory-record-list) - - [`agentcore memory actor`](#agentcore-memory-actor) - - [`agentcore memory actor list`](#agentcore-memory-actor-list) - - [`agentcore memory session`](#agentcore-memory-session) - - [`agentcore memory session list`](#agentcore-memory-session-list) -- [Gateway commands](#gateway-commands) - - [`agentcore gateway`](#agentcore-gateway) - - [`agentcore gateway get`](#agentcore-gateway-get) - - [`agentcore gateway list`](#agentcore-gateway-list) - - [`agentcore gateway invoke`](#agentcore-gateway-invoke) - - [`agentcore gateway target`](#agentcore-gateway-target) - - [`agentcore gateway target get`](#agentcore-gateway-target-get) - - [`agentcore gateway target list`](#agentcore-gateway-target-list) - - [`agentcore gateway connector`](#agentcore-gateway-connector) - - [`agentcore gateway connector get`](#agentcore-gateway-connector-get) - - [`agentcore gateway connector list`](#agentcore-gateway-connector-list) - - [`agentcore gateway rule`](#agentcore-gateway-rule) - - [`agentcore gateway rule get`](#agentcore-gateway-rule-get) - - [`agentcore gateway rule list`](#agentcore-gateway-rule-list) - - [`agentcore gateway policy`](#agentcore-gateway-policy) - - [`agentcore gateway policy generate`](#agentcore-gateway-policy-generate) -- [Payment commands](#payment-commands) - - [`agentcore payment`](#agentcore-payment) - - [`agentcore payment manager`](#agentcore-payment-manager) - - [`agentcore payment manager get`](#agentcore-payment-manager-get) - - [`agentcore payment manager list`](#agentcore-payment-manager-list) - - [`agentcore payment connector`](#agentcore-payment-connector) - - [`agentcore payment connector get`](#agentcore-payment-connector-get) - - [`agentcore payment connector list`](#agentcore-payment-connector-list) - - [`agentcore payment session`](#agentcore-payment-session) - - [`agentcore payment session get`](#agentcore-payment-session-get) - - [`agentcore payment session list`](#agentcore-payment-session-list) - - [`agentcore payment instrument`](#agentcore-payment-instrument) - - [`agentcore payment instrument get`](#agentcore-payment-instrument-get) - - [`agentcore payment instrument list`](#agentcore-payment-instrument-list) - - [`agentcore payment instrument balance`](#agentcore-payment-instrument-balance) - [Evaluation commands](#evaluation-commands) - [`agentcore eval`](#agentcore-eval) - [`agentcore eval evaluator`](#agentcore-eval-evaluator) @@ -977,1256 +877,6 @@ agentcore project build [options] build the project's deployable artifacts -## Harness commands - -### `agentcore harness` - -```text -agentcore harness [options] [command] -``` - -manage AgentCore harnesses - -#### `agentcore harness create` - -```text -agentcore harness create [options] -``` - -create a harness - -**Options** - -- `--name `: the name of the harness (required) -- `--execution-role-arn `: IAM role the harness assumes; a default role is created when omitted -- `--system-prompt `: the agent's system prompt -- `--model `: model configuration (JSON HarnessModelConfiguration) -- `--tools `: tools available to the agent (JSON HarnessTool[]) -- `--skills `: skills available to the agent (JSON HarnessSkill[]) -- `--allowed-tools `: tool allowlist patterns (e.g. \* or @serverName/toolName) -- `--memory `: memory configuration (JSON HarnessMemoryConfiguration) -- `--truncation `: context truncation configuration (JSON HarnessTruncationConfiguration) -- `--environment `: compute environment configuration (JSON HarnessEnvironmentProviderRequest) -- `--environment-artifact `: environment artifact, e.g. a container image (JSON HarnessEnvironmentArtifact) -- `--environment-variables `: environment variables (JSON object of key/value strings) -- `--authorizer-configuration `: inbound authorizer configuration (JSON AuthorizerConfiguration) -- `--max-iterations `: max agent loop iterations per invocation -- `--max-tokens `: max total output tokens per invocation -- `--timeout-seconds `: max duration in seconds per invocation -- `--tags `: tags to apply (JSON object of key/value strings) - -#### `agentcore harness get` - -```text -agentcore harness get [options] -``` - -get a harness - -**Options** - -- `--id `: the ID of the harness (required) - -#### `agentcore harness list` - -```text -agentcore harness list [options] -``` - -list harnesses - -**Options** - -- `--next-token `: next token to use on paginated -- `--max-results `: max number of items to return - -#### `agentcore harness update` - -```text -agentcore harness update [options] -``` - -update a harness (creates a new version) - -**Options** - -- `--id `: the ID of the harness to update (required) -- `--execution-role-arn `: IAM role the harness assumes -- `--system-prompt `: the agent's system prompt -- `--model `: model configuration (JSON HarnessModelConfiguration) -- `--tools `: tools available to the agent (JSON HarnessTool[]) -- `--skills `: skills available to the agent (JSON HarnessSkill[]) -- `--allowed-tools `: tool allowlist patterns (e.g. \* or @serverName/toolName) -- `--memory `: memory configuration (JSON HarnessMemoryConfiguration) -- `--clear-memory `: clear the memory configuration (pass true) -- `--truncation `: context truncation configuration (JSON HarnessTruncationConfiguration) -- `--environment `: compute environment configuration (JSON HarnessEnvironmentProviderRequest) -- `--environment-artifact `: environment artifact, e.g. a container image (JSON HarnessEnvironmentArtifact) -- `--clear-environment-artifact `: clear the environment artifact (pass true) -- `--environment-variables `: environment variables (JSON object; replaces all existing) -- `--authorizer-configuration `: inbound authorizer configuration (JSON AuthorizerConfiguration) -- `--clear-authorizer-configuration `: clear the authorizer configuration (pass true) -- `--max-iterations `: max agent loop iterations per invocation -- `--max-tokens `: max total output tokens per invocation -- `--timeout-seconds `: max duration in seconds per invocation - -#### `agentcore harness delete` - -```text -agentcore harness delete [options] -``` - -delete a harness - -**Options** - -- `--id `: the ID of the harness to delete (required) -- `--delete-managed-memory `: whether to also delete the managed Memory (default true; pass false to keep it) - -#### `agentcore harness invoke` - -```text -agentcore harness invoke [options] -``` - -invoke a harness - -**Options** - -- `--id `: the ID of the harness (required) -- `--prompt `: the message to send to the harness -- `--session-id `: the Runtime session ID to continue (33-100 characters) -- `--qualifier `: the harness endpoint qualifier to invoke (default DEFAULT) - -#### `agentcore harness exec` - -```text -agentcore harness exec [options] -``` - -run a shell command in a harness - -**Options** - -- `--id `: the ID of the harness (required) -- `--command `: the shell command to run -- `--session-id `: the Runtime session ID to run in (33-100 characters) -- `--qualifier `: the harness endpoint qualifier to run in (default DEFAULT) -- `--timeout `: seconds to wait for the command (1-3600) - -#### `agentcore harness logs` - -```text -agentcore harness logs [options] -``` - -stream or search a harness's logs - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the harness endpoint qualifier -- `--since `: search window start: "5m", "1h", ISO 8601, epoch ms, or "now" -- `--until `: search window end: "5m", "1h", ISO 8601, epoch ms, or "now" -- `--tail`: tail new log records (default: false) -- `--level `: filter by log level (error, warn, info, debug) -- `--query `: CloudWatch Logs filter pattern -- `--limit `: maximum number of log records to return in search mode - -#### `agentcore harness traces` - -```text -agentcore harness traces [options] [command] -``` - -inspect a harness's traces - -##### `agentcore harness traces list` - -```text -agentcore harness traces list [options] -``` - -list a harness's recent traces - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the harness endpoint qualifier -- `--limit `: maximum number of traces to display (default: 20) -- `--since `: window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago) -- `--until `: window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now) - -##### `agentcore harness traces get` - -```text -agentcore harness traces get [options] -``` - -download a harness trace's log records to a JSON file - -**Arguments** - -- `trace-id` (required): the trace ID to download - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the harness endpoint qualifier -- `--output `: the output file path (default: <traceId>.json in the current directory) -- `--since `: window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago) -- `--until `: window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now) - -#### `agentcore harness endpoint` - -```text -agentcore harness endpoint [options] [command] -``` - -manage harness endpoints - -##### `agentcore harness endpoint create` - -```text -agentcore harness endpoint create [options] -``` - -create a harness endpoint - -**Options** - -- `--id `: the ID of the harness (required) -- `--name `: the name of the endpoint (required) -- `--target-version `: the harness version the endpoint points to (default latest) -- `--tags `: tags to apply (JSON object of key/value strings) - -##### `agentcore harness endpoint get` - -```text -agentcore harness endpoint get [options] -``` - -get a harness endpoint - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the endpoint name (qualifier) (required) - -##### `agentcore harness endpoint list` - -```text -agentcore harness endpoint list [options] -``` - -list a harness's endpoints - -**Options** - -- `--id `: the ID of the harness (required) -- `--next-token `: next token to use on paginated -- `--max-results `: max number of items to return - -##### `agentcore harness endpoint update` - -```text -agentcore harness endpoint update [options] -``` - -update a harness endpoint - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the endpoint name (qualifier) (required) -- `--target-version `: the harness version the endpoint points to - -##### `agentcore harness endpoint delete` - -```text -agentcore harness endpoint delete [options] -``` - -delete a harness endpoint - -**Options** - -- `--id `: the ID of the harness (required) -- `--qualifier `: the endpoint name (qualifier) (required) - -#### `agentcore harness version` - -```text -agentcore harness version [options] [command] -``` - -inspect harness versions - -##### `agentcore harness version get` - -```text -agentcore harness version get [options] -``` - -get a specific version of a harness - -**Options** - -- `--id `: the ID of the harness (required) -- `--version `: the harness version to get (required) - -##### `agentcore harness version list` - -```text -agentcore harness version list [options] -``` - -list a harness's versions - -**Options** - -- `--id `: the ID of the harness (required) -- `--next-token `: next token to use on paginated -- `--max-results `: max number of items to return - -## Identity commands - -### `agentcore identity` - -```text -agentcore identity [options] [command] -``` - -manage AgentCore Identity resources - -#### `agentcore identity api-key-credential-provider` - -```text -agentcore identity api-key-credential-provider [options] [command] -``` - -manage API key credential providers - -##### `agentcore identity api-key-credential-provider create` - -```text -agentcore identity api-key-credential-provider create [options] -``` - -create an API key credential provider - -**Options** - -- `--name `: the name of the API key credential provider (required) -- `--api-key `: the API key (file://path or - for stdin; inline values are rejected) -- `--api-key-secret-reference `: external secret reference JSON: {"secretId":"<arn>","jsonKey":"<key>"} -- `--tags `: tags as key=value (repeatable) or JSON object - -##### `agentcore identity api-key-credential-provider get` - -```text -agentcore identity api-key-credential-provider get [options] -``` - -get an API key credential provider - -**Options** - -- `--name `: the name of the API key credential provider (required) - -##### `agentcore identity api-key-credential-provider list` - -```text -agentcore identity api-key-credential-provider list [options] -``` - -list API key credential providers - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -##### `agentcore identity api-key-credential-provider update` - -```text -agentcore identity api-key-credential-provider update [options] -``` - -update an API key credential provider - -**Options** - -- `--name `: the name of the API key credential provider (required) -- `--api-key `: the new API key (file://path or - for stdin; inline values are rejected) -- `--api-key-secret-reference `: external secret reference JSON: {"secretId":"<arn>","jsonKey":"<key>"} - -##### `agentcore identity api-key-credential-provider delete` - -```text -agentcore identity api-key-credential-provider delete [options] -``` - -delete an API key credential provider - -**Options** - -- `--name `: the name of the API key credential provider (required) - -#### `agentcore identity oauth2-credential-provider` - -```text -agentcore identity oauth2-credential-provider [options] [command] -``` - -manage OAuth2 credential providers - -##### `agentcore identity oauth2-credential-provider create` - -```text -agentcore identity oauth2-credential-provider create [options] -``` - -create an OAuth2 credential provider - -**Options** - -- `--name `: the name of the OAuth2 credential provider (required) -- `--vendor `: the OAuth2 vendor (e.g. CustomOauth2, GithubOauth2) (required) -- `--client-secret `: the client secret (file://path or - for stdin; inline values are rejected) -- `--client-secret-reference `: external secret reference JSON: {"secretId":"<arn>","jsonKey":"<key>"} -- `--client-id `: OAuth2 client ID (guided custom OAuth2) -- `--discovery-url `: OAuth2 discovery URL (guided custom OAuth2) -- `--authorization-server-metadata `: authorization server metadata JSON (guided custom OAuth2) -- `--provider-configuration `: complete OAuth2 provider configuration JSON (alternative to guided flags) -- `--tags `: tags as key=value (repeatable) or JSON object - -##### `agentcore identity oauth2-credential-provider get` - -```text -agentcore identity oauth2-credential-provider get [options] -``` - -get an OAuth2 credential provider - -**Options** - -- `--name `: the name of the OAuth2 credential provider (required) - -##### `agentcore identity oauth2-credential-provider list` - -```text -agentcore identity oauth2-credential-provider list [options] -``` - -list OAuth2 credential providers - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -##### `agentcore identity oauth2-credential-provider update` - -```text -agentcore identity oauth2-credential-provider update [options] -``` - -update an OAuth2 credential provider - -**Options** - -- `--name `: the name of the OAuth2 credential provider (required) -- `--vendor `: the OAuth2 vendor -- `--client-secret `: the client secret (file://path or - for stdin; inline values are rejected) -- `--client-secret-reference `: external secret reference JSON: {"secretId":"<arn>","jsonKey":"<key>"} -- `--client-id `: OAuth2 client ID (guided custom OAuth2) -- `--discovery-url `: OAuth2 discovery URL (guided custom OAuth2) -- `--authorization-server-metadata `: authorization server metadata JSON (guided custom OAuth2) -- `--provider-configuration `: complete OAuth2 provider configuration JSON (alternative to guided flags) - -##### `agentcore identity oauth2-credential-provider delete` - -```text -agentcore identity oauth2-credential-provider delete [options] -``` - -delete an OAuth2 credential provider - -**Options** - -- `--name `: the name of the OAuth2 credential provider (required) - -#### `agentcore identity payment-credential-provider` - -```text -agentcore identity payment-credential-provider [options] [command] -``` - -manage payment credential providers - -##### `agentcore identity payment-credential-provider get` - -```text -agentcore identity payment-credential-provider get [options] -``` - -get a payment credential provider - -**Options** - -- `--name `: the payment credential provider name (required) - -##### `agentcore identity payment-credential-provider list` - -```text -agentcore identity payment-credential-provider list [options] -``` - -list payment credential providers - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -## Runtime commands - -### `agentcore runtime` - -```text -agentcore runtime [options] [command] -``` - -inspect AgentCore Runtimes - -#### `agentcore runtime get` - -```text -agentcore runtime get [options] -``` - -get an AgentCore Runtime - -**Options** - -- `--id `: the ID of the Runtime (required) - -#### `agentcore runtime list` - -```text -agentcore runtime list [options] -``` - -list AgentCore Runtimes - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore runtime invoke` - -```text -agentcore runtime invoke [options] -``` - -invoke a Runtime - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--payload `: the inline payload to send -- `--qualifier `: the Runtime endpoint qualifier -- `--content-type `: the payload content type -- `--accept `: the accepted response content type -- `--session-id `: the Runtime session ID -- `--user-id `: the Runtime user ID (default "default") -- `--header `: an ordered application header -- `--bearer-token `: the CUSTOM\_JWT bearer token -- `--mcp-session-id `: the MCP session ID -- `--mcp-protocol-version `: the MCP protocol version -- `--mcp-method `: the MCP method -- `--mcp-name `: the MCP tool, resource, or prompt name -- `--trace-id `: the X-Ray trace ID -- `--trace-parent `: the W3C trace parent -- `--trace-state `: the W3C trace state -- `--baggage `: the W3C baggage -- `--output-file `: the response output file - -#### `agentcore runtime shell` - -```text -agentcore runtime shell [options] -``` - -open an interactive shell in a Runtime - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--qualifier `: the Runtime endpoint qualifier -- `--session-id `: the Runtime session ID to use -- `--bearer-token `: the CUSTOM\_JWT bearer token - -#### `agentcore runtime version` - -```text -agentcore runtime version [options] [command] -``` - -inspect AgentCore Runtime versions - -##### `agentcore runtime version get` - -```text -agentcore runtime version get [options] -``` - -get a specific Runtime version - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--version `: the Runtime version to get (required) - -##### `agentcore runtime version list` - -```text -agentcore runtime version list [options] -``` - -list a Runtime's versions - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore runtime endpoint` - -```text -agentcore runtime endpoint [options] [command] -``` - -inspect AgentCore Runtime endpoints - -##### `agentcore runtime endpoint get` - -```text -agentcore runtime endpoint get [options] -``` - -get a Runtime endpoint - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--qualifier `: the endpoint name (qualifier) (required) - -##### `agentcore runtime endpoint list` - -```text -agentcore runtime endpoint list [options] -``` - -list a Runtime's endpoints - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore runtime logs` - -```text -agentcore runtime logs [options] -``` - -stream or search a Runtime's logs - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--qualifier `: the Runtime endpoint qualifier -- `--since `: search window start: "5m", "1h", ISO 8601, epoch ms, or "now" -- `--until `: search window end: "5m", "1h", ISO 8601, epoch ms, or "now" -- `--tail`: tail new log records (default: false) -- `--level `: filter by log level (error, warn, info, debug) -- `--query `: CloudWatch Logs filter pattern -- `--limit `: maximum number of log records to return in search mode - -#### `agentcore runtime traces` - -```text -agentcore runtime traces [options] [command] -``` - -inspect a Runtime's traces - -##### `agentcore runtime traces list` - -```text -agentcore runtime traces list [options] -``` - -list a Runtime's recent traces - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--qualifier `: the Runtime endpoint qualifier -- `--limit `: maximum number of traces to display (default: 20) -- `--since `: window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago) -- `--until `: window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now) - -##### `agentcore runtime traces get` - -```text -agentcore runtime traces get [options] -``` - -download a trace's log records to a JSON file - -**Arguments** - -- `trace-id` (required): the trace ID to download - -**Options** - -- `--id `: the ID of the Runtime (required) -- `--qualifier `: the Runtime endpoint qualifier -- `--output `: the output file path (default: <traceId>.json in the current directory) -- `--since `: window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago) -- `--until `: window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now) - -## Memory commands - -### `agentcore memory` - -```text -agentcore memory [options] [command] -``` - -inspect AgentCore Memories - -#### `agentcore memory get` - -```text -agentcore memory get [options] -``` - -get an AgentCore Memory - -**Options** - -- `--id `: the ID of the Memory (required) -- `--view `: response view - -#### `agentcore memory list` - -```text -agentcore memory list [options] -``` - -list AgentCore Memories - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore memory event` - -```text -agentcore memory event [options] [command] -``` - -inspect AgentCore Memory events - -##### `agentcore memory event get` - -```text -agentcore memory event get [options] -``` - -get an AgentCore Memory Event - -**Options** - -- `--id `: the ID of the Memory (required) -- `--actor-id `: the ID of the actor (required) -- `--session-id `: the session ID (required) -- `--event-id `: the event ID (required) - -##### `agentcore memory event list` - -```text -agentcore memory event list [options] -``` - -list AgentCore Memory events - -**Options** - -- `--id `: the ID of the Memory (required) -- `--actor-id `: the ID of the actor (required) -- `--session-id `: the session ID (required) -- `--include-payloads`: includes event payloads in the response (default: false) -- `--branch `: filter events by branch name -- `--include-parent-branches`: includes parent branches when filtering by branch (default: false) -- `--metadata-filters `: event metadata filters as JSON -- `--max-results `: maximum number of events to return; default 20 -- `--next-token `: pagination token returned by a previous request - -#### `agentcore memory record` - -```text -agentcore memory record [options] [command] -``` - -inspect AgentCore Memory records - -##### `agentcore memory record get` - -```text -agentcore memory record get [options] -``` - -get an AgentCore Memory record - -**Options** - -- `--id `: the ID of the Memory (required) -- `--record-id `: the ID of the Memory record (required) - -##### `agentcore memory record list` - -```text -agentcore memory record list [options] -``` - -list AgentCore Memory records - -**Options** - -- `--id `: the ID of the Memory (required) -- `--namespace `: filter by namespace prefix -- `--namespace-path `: filter by namespace hierarchy -- `--strategy-id `: filter by Memory strategy ID -- `--metadata-filters `: Memory record metadata filters as JSON -- `--max-results `: maximum number of records to return -- `--next-token `: pagination token returned by a previous request - -#### `agentcore memory actor` - -```text -agentcore memory actor [options] [command] -``` - -inspect actors in AgentCore Memories - -##### `agentcore memory actor list` - -```text -agentcore memory actor list [options] -``` - -list actors in an AgentCore Memory - -**Options** - -- `--id `: the ID of the Memory (required) -- `--max-results `: maximum number of actors to return -- `--next-token `: pagination token returned by a previous request - -#### `agentcore memory session` - -```text -agentcore memory session [options] [command] -``` - -inspect sessions in AgentCore Memories - -##### `agentcore memory session list` - -```text -agentcore memory session list [options] -``` - -list sessions in an AgentCore Memory - -**Options** - -- `--id `: the ID of the Memory (required) -- `--actor-id `: the ID of the actor (required) -- `--max-results `: maximum number of sessions to return -- `--next-token `: pagination token returned by a previous request - -## Gateway commands - -### `agentcore gateway` - -```text -agentcore gateway [options] [command] -``` - -manage AgentCore Gateways - -#### `agentcore gateway get` - -```text -agentcore gateway get [options] -``` - -get an AgentCore Gateway - -**Options** - -- `--id `: the ID of the Gateway (required) - -#### `agentcore gateway list` - -```text -agentcore gateway list [options] -``` - -list AgentCore Gateways - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore gateway invoke` - -```text -agentcore gateway invoke [options] -``` - -invoke an AgentCore Gateway - -**Options** - -- `--id `: the ID of the Gateway (required) -- `--path `: the path relative to the Gateway origin -- `--method `: the HTTP request method -- `--payload `: the inline payload to send -- `--content-type `: the payload content type -- `--accept `: the accepted response content type -- `--header `: an ordered application header -- `--bearer-token `: the Gateway bearer token -- `--session-id `: the Runtime target session ID -- `--mcp-session-id `: the MCP session ID -- `--mcp-protocol-version `: the MCP protocol version -- `--output-file `: the response output file - -#### `agentcore gateway target` - -```text -agentcore gateway target [options] [command] -``` - -manage Targets for an AgentCore Gateway - -##### `agentcore gateway target get` - -```text -agentcore gateway target get [options] -``` - -get a Gateway Target - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--target-id `: the ID of the Gateway Target (required) - -##### `agentcore gateway target list` - -```text -agentcore gateway target list [options] -``` - -list Targets for an AgentCore Gateway - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore gateway connector` - -```text -agentcore gateway connector [options] [command] -``` - -manage connectors configured for an AgentCore Gateway - -##### `agentcore gateway connector get` - -```text -agentcore gateway connector get [options] -``` - -get a connector-backed Gateway Target - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--id `: the ID of the connector-backed Gateway Target (required) - -##### `agentcore gateway connector list` - -```text -agentcore gateway connector list [options] -``` - -list connectors configured for an AgentCore Gateway - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore gateway rule` - -```text -agentcore gateway rule [options] [command] -``` - -manage Rules for an AgentCore Gateway - -##### `agentcore gateway rule get` - -```text -agentcore gateway rule get [options] -``` - -get a Gateway Rule - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--rule-id `: the ID of the Gateway Rule (required) - -##### `agentcore gateway rule list` - -```text -agentcore gateway rule list [options] -``` - -list Rules for an AgentCore Gateway - -**Options** - -- `--gateway-id `: the ID of the Gateway (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore gateway policy` - -```text -agentcore gateway policy [options] [command] -``` - -generate Cedar policies for an AgentCore Gateway - -##### `agentcore gateway policy generate` - -```text -agentcore gateway policy generate [options] -``` - -generate a Cedar policy for a Gateway from a natural-language prompt - -**Options** - -- `--gateway-id `: the ID or ARN of the Gateway the policy applies to (required) -- `--policy-engine-id `: the ID or ARN of the Policy Engine (default the Gateway's attached engine) -- `--prompt `: what the policy should allow or deny (inline, file://<path>, or - for stdin) -- `--name `: name of the generation request (default cli\_generation\_<timestamp>) - -## Payment commands - -### `agentcore payment` - -```text -agentcore payment [options] [command] -``` - -manage AgentCore Payments - -#### `agentcore payment manager` - -```text -agentcore payment manager [options] [command] -``` - -manage AgentCore payment managers - -##### `agentcore payment manager get` - -```text -agentcore payment manager get [options] -``` - -get a payment manager by id - -**Options** - -- `--id `: the payment manager ID (required) - -##### `agentcore payment manager list` - -```text -agentcore payment manager list [options] -``` - -list payment managers - -**Options** - -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore payment connector` - -```text -agentcore payment connector [options] [command] -``` - -manage connectors under a payment manager - -##### `agentcore payment connector get` - -```text -agentcore payment connector get [options] -``` - -get a payment connector by id - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--connector-id `: the payment connector ID (required) - -##### `agentcore payment connector list` - -```text -agentcore payment connector list [options] -``` - -list the connectors of a payment manager - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return - -#### `agentcore payment session` - -```text -agentcore payment session [options] [command] -``` - -manage payment sessions (budget-limited payment contexts) - -##### `agentcore payment session get` - -```text -agentcore payment session get [options] -``` - -get a payment session by id - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--session-id `: the payment session ID (required) -- `--user-id `: the application user ID associated with the session (required) -- `--agent-name `: optional observability label, not an agent selector - -##### `agentcore payment session list` - -```text -agentcore payment session list [options] -``` - -list an application user's payment sessions under a payment manager - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--user-id `: the application user ID to list sessions for (required) -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return -- `--agent-name `: optional observability label, not an agent selector - -#### `agentcore payment instrument` - -```text -agentcore payment instrument [options] [command] -``` - -manage payment instruments (embedded crypto wallets) - -##### `agentcore payment instrument get` - -```text -agentcore payment instrument get [options] -``` - -get a payment instrument by id - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--instrument-id `: the payment instrument ID (required) -- `--user-id `: the application user ID associated with the instrument (required) -- `--connector-id `: optionally restrict the lookup to this payment connector -- `--agent-name `: optional observability label, not an agent selector - -##### `agentcore payment instrument list` - -```text -agentcore payment instrument list [options] -``` - -list an application user's payment instruments under a payment manager - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--user-id `: the application user ID to list instruments for (required) -- `--connector-id `: optionally filter instruments by this payment connector -- `--next-token `: pagination token returned by a previous request -- `--max-results `: maximum number of items to return -- `--agent-name `: optional observability label, not an agent selector - -##### `agentcore payment instrument balance` - -```text -agentcore payment instrument balance [options] -``` - -get a payment instrument's token balance on a specific chain - -**Options** - -- `--manager-id `: the parent payment manager ID (required) -- `--connector-id `: the instrument's payment connector ID (required) -- `--instrument-id `: the payment instrument ID (required) -- `--user-id `: the application user ID associated with the instrument (required) -- `--chain `: the blockchain chain to query (BASE | BASE\_SEPOLIA | ETHEREUM | SOLANA | SOLANA\_DEVNET) (required) -- `--token `: the token to query (USDC) (default: "USDC") -- `--agent-name `: optional observability label, not an agent selector - ## Evaluation commands ### `agentcore eval` From a928c43fcb6c2f8c37a59fedb22726a00baaf1f8 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 14:27:26 +0000 Subject: [PATCH 06/11] docs: simplify default command reference groups --- scripts/generate-command-reference.mjs | 29 +++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/scripts/generate-command-reference.mjs b/scripts/generate-command-reference.mjs index 068cbab281..82d6552d28 100644 --- a/scripts/generate-command-reference.mjs +++ b/scripts/generate-command-reference.mjs @@ -10,12 +10,6 @@ const REPOSITORY_ROOT = resolve(SCRIPT_DIR, ".."); const DEFAULT_GROUPS = [ { id: "global-options", title: "Global options", commands: [] }, { id: "project", title: "Project commands", commands: ["project"] }, - { id: "harness", title: "Harness commands", commands: ["harness"], optional: true }, - { id: "identity", title: "Identity commands", commands: ["identity"], optional: true }, - { id: "runtime", title: "Runtime commands", commands: ["runtime"], optional: true }, - { id: "memory", title: "Memory commands", commands: ["memory"], optional: true }, - { id: "gateway", title: "Gateway commands", commands: ["gateway"], optional: true }, - { id: "payment", title: "Payment commands", commands: ["payment"], optional: true }, { id: "evaluation", title: "Evaluation commands", commands: ["eval"] }, { id: "settings", @@ -182,10 +176,7 @@ function buildModel({ version, groups = DEFAULT_GROUPS, env }) { const discovered = new Set(rootEntry.members.map((entry) => entry.name.split(" ").at(-1))); const grouped = new Set(groups.flatMap((group) => group.commands)); const missing = [...discovered].filter((command) => !grouped.has(command)); - const unknown = groups - .filter((group) => !group.optional) - .flatMap((group) => group.commands) - .filter((command) => !discovered.has(command)); + const unknown = [...grouped].filter((command) => !discovered.has(command)); if (missing.length) { throw new Error(`Top-level commands missing from groups: ${missing.join(", ")}`); @@ -200,17 +191,13 @@ function buildModel({ version, groups = DEFAULT_GROUPS, env }) { return { version, - groups: groups - .filter( - (group) => !group.optional || group.commands.some((command) => discovered.has(command)), - ) - .map((group) => ({ - title: group.title, - entries: - group.id === "global-options" - ? [{ ...rootEntry, members: [] }] - : group.commands.map((command) => entriesByCommand.get(command)), - })), + groups: groups.map((group) => ({ + title: group.title, + entries: + group.id === "global-options" + ? [{ ...rootEntry, members: [] }] + : group.commands.map((command) => entriesByCommand.get(command)), + })), }; } From ca78fa5ed53d15ac5e9936499cec85e2ef82238b Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 15:39:51 +0000 Subject: [PATCH 07/11] test(cli): consolidate imperative command coverage --- src/components/CliOnlyScreen.test.tsx | 21 +-- src/components/RouterScreen.test.tsx | 26 ++++ src/handlers/imperative.test.tsx | 197 -------------------------- src/handlers/root.test.tsx | 80 ++++++++--- 4 files changed, 100 insertions(+), 224 deletions(-) delete mode 100644 src/handlers/imperative.test.tsx diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index de94522780..66e0ecff06 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -137,15 +137,18 @@ describe("every command-line-only command opens on screen", () => { }); describe("paths without a screen of their own", () => { - test("an unknown path retains the standard help fallback", async () => { - const r = renderScreen("/agentcore/gateway/no-such-command"); - - await waitForText(() => r.frames.join("\n"), "Usage:"); - const output = r.frames.join("\n"); - expect(output).toMatch(/^\s+create\s+/m); - expect(output).not.toContain("command line only"); - r.unmount(); - }); + test.each(["/agentcore/gateway/no-such-command", "/agentcore/payment"])( + "%s retains the standard help fallback", + async (path) => { + const r = renderScreen(path); + + await waitForText(() => r.frames.join("\n"), "Usage:"); + const output = r.frames.join("\n"); + expect(output).toMatch(/^\s+create\s+/m); + expect(output).not.toContain("command line only"); + r.unmount(); + }, + ); test("a group drills down to a leaf's help and back", async () => { const r = renderScreen("/agentcore/gateway", { globalConfig: MUTATION_CONFIG }); diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index f8a2956d35..3dd9893295 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -31,6 +31,18 @@ describe("menu rendering", () => { r.unmount(); }); + test("lists standalone commands in the root menu when enabled", async () => { + const r = renderImperativeScreen("/agentcore"); + await waitForText(r.lastFrame, "type to choose a command"); + + const entries = menuEntries(r.lastFrame()!); + expect(entries.screens).toEqual( + expect.arrayContaining(["harness", "identity", "runtime", "memory", "gateway"]), + ); + expect(entries.cliOnly).toContain("payment"); + r.unmount(); + }); + test("shows the command description in the header", async () => { const r = renderScreen("/agentcore"); await waitForText(r.lastFrame, "the platform for production AI agents"); @@ -92,6 +104,20 @@ describe("filtering", () => { }); describe("navigation", () => { + test.each(["harness", "runtime/endpoint"])( + "an unavailable %s menu redirects to a working root menu", + async (path) => { + const r = renderScreen(`/agentcore/${path}`); + await waitForText(r.lastFrame, "the platform for production AI agents"); + + await r.write("eval"); + await r.press("return"); + await waitForText(r.lastFrame, "agentcore → eval"); + expect(r.lastFrame()).toContain("evaluator"); + r.unmount(); + }, + ); + test("down arrow moves the highlight to the next option", async () => { const r = renderScreen("/agentcore"); await waitForText(r.lastFrame, "❯ create"); diff --git a/src/handlers/imperative.test.tsx b/src/handlers/imperative.test.tsx deleted file mode 100644 index 4fcf2421c2..0000000000 --- a/src/handlers/imperative.test.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; -import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; -import { compile, ValueContext } from "../router"; -import { - cleanupScreens, - createSilentLogger, - menuEntries, - renderScreen, - TestCoreClient, - TestGlobalConfigAccessor, - testIO, - waitForText, -} from "../testing"; -import { createRootHandler } from "./index"; - -afterEach(() => { - cleanupScreens(); - mock.restore(); -}); - -const FAMILIES = ["harness", "identity", "runtime", "memory", "gateway", "payment"] as const; -const PROJECT_COMMANDS = [ - "create", - "add", - "export", - "remove", - "dev", - "deploy", - "invoke", - "log", - "traces", - "status", - "build", -]; -const PROJECT_SCREENS = ["create", "add", "remove", "deploy", "invoke", "status", "build"]; -const PROJECT_CLI_ONLY = ["export", "dev", "log", "traces"]; -const PUBLIC_COMMANDS = [...PROJECT_COMMANDS, "eval", "feedback", "config", "update"]; -const ALL_COMMANDS = [...PROJECT_COMMANDS, ...FAMILIES, "eval", "feedback", "config", "update"]; -const STATES = [undefined, false, true].flatMap((enabled) => - [false, true].map((mutations) => ({ enabled, mutations })), -); - -function configFor(enabled: boolean | undefined, mutations: boolean) { - return { - ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": mutations, - ...(enabled === undefined ? {} : { "imperative-commands": enabled }), - }; -} - -function setup(enabled: boolean | undefined, mutations: boolean) { - const core = new TestCoreClient(); - const paymentCalls = [ - spyOn(core.payment, "getPaymentManager"), - spyOn(core.payment, "listPaymentManagers"), - spyOn(core.payment, "getPaymentConnector"), - spyOn(core.payment, "listPaymentConnectors"), - spyOn(core.payment, "getPaymentSession"), - spyOn(core.payment, "listPaymentSessions"), - spyOn(core.payment, "getPaymentInstrument"), - spyOn(core.payment, "getPaymentInstrumentBalance"), - spyOn(core.payment, "listPaymentInstruments"), - ]; - const globalConfig = configFor(enabled, mutations); - const root = createRootHandler(core, { - io: testIO().io, - logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: globalConfig }), - globalConfig, - }); - const command = compile(root, ValueContext.EmptyContext()); - command.configureOutput({ writeErr: () => {}, writeOut: () => {} }); - const expectNoCoreCalls = () => { - for (const family of FAMILIES) { - if (family !== "payment") expect(core[family].calls).toEqual([]); - } - for (const call of paymentCalls) expect(call).not.toHaveBeenCalled(); - }; - return { core, command, expectNoCoreCalls }; -} - -describe("imperative command families", () => { - test.each(STATES)( - "root flag $enabled and Gateway mutation flag $mutations select the command tree", - ({ enabled, mutations }) => { - const { command } = setup(enabled, mutations); - expect(command.commands.map((child) => child.name())).toEqual( - enabled ? ALL_COMMANDS : PUBLIC_COMMANDS, - ); - for (const family of FAMILIES) { - expect(new RegExp(`\\n\\s+${family}\\s`).test(command.helpInformation())).toBe( - enabled === true, - ); - } - if (enabled) { - const gateway = command.commands.find((child) => child.name() === "gateway")!; - const groups = [ - gateway, - ...gateway.commands.filter((child) => - ["target", "connector", "rule"].includes(child.name()), - ), - ]; - for (const group of groups) { - for (const mutation of ["create", "update", "delete"]) { - expect(group.commands.some((child) => child.name() === mutation)).toBe(mutations); - } - } - } - expect( - command.commands - .find((child) => child.name() === "add") - ?.commands.map((child) => child.name()), - ).toEqual(expect.arrayContaining(["harness", "runtime", "memory", "gateway"])); - const evaluation = command.commands.find((child) => child.name() === "eval")!; - expect(evaluation.commands.map((child) => child.name())).toContain("evaluator"); - }, - ); - - test.each([...FAMILIES])( - "disabled %s rejects CLI requests without calling Core", - async (family) => { - for (const mutations of [false, true]) { - for (const args of [[], ["list", "--json"], ["get", "--id", "disabled"]]) { - const { command, expectNoCoreCalls } = setup(false, mutations); - await expect( - command.parseAsync(["node", "agentcore", family, ...args]), - ).rejects.toThrow(); - expectNoCoreCalls(); - } - } - }, - ); - - test("root registration uses the resolved snapshot without reading config", () => { - const accessor = new TestGlobalConfigAccessor(); - let reads = 0; - accessor.get = async () => { - reads++; - throw new Error("Must not read config during construction"); - }; - const root = createRootHandler(new TestCoreClient(), { - io: testIO().io, - logger: createSilentLogger(), - globalConfigAccessor: accessor, - globalConfig: configFor(false, true), - }); - expect(root.children().map((child) => child.name())).toEqual(PUBLIC_COMMANDS); - expect(reads).toBe(0); - }); -}); - -describe("imperative command menus", () => { - test.each(STATES)( - "root menu matches parent $enabled and Gateway mutation $mutations", - async ({ enabled, mutations }) => { - const { core, expectNoCoreCalls } = setup(enabled, mutations); - const screen = renderScreen("/agentcore", { - core, - globalConfig: configFor(enabled, mutations), - }); - await waitForText(screen.lastFrame, "type to choose a command"); - expect(menuEntries(screen.lastFrame()!)).toEqual({ - screens: enabled - ? [...PROJECT_SCREENS, "harness", "identity", "runtime", "memory", "gateway", "eval"] - : [...PROJECT_SCREENS, "eval"], - cliOnly: enabled - ? [...PROJECT_CLI_ONLY, "payment", "feedback", "config", "update"] - : [...PROJECT_CLI_ONLY, "feedback", "config", "update"], - }); - expectNoCoreCalls(); - }, - ); - - test.each([ - ...FAMILIES.filter((family) => family !== "payment"), - "runtime/endpoint", - "harness/version", - "gateway/target", - ])("unavailable menu %s returns to a registered menu", async (path) => { - const { core, expectNoCoreCalls } = setup(false, false); - const screen = renderScreen(`/agentcore/${path}`, { core }); - await waitForText(screen.lastFrame, "the platform for production AI agents"); - expect(menuEntries(screen.lastFrame()!)).toEqual({ - screens: [...PROJECT_SCREENS, "eval"], - cliOnly: [...PROJECT_CLI_ONLY, "feedback", "config", "update"], - }); - expectNoCoreCalls(); - }); - - test("an unavailable CLI-only family retains the unknown-route fallback", async () => { - const { core, expectNoCoreCalls } = setup(false, false); - const screen = renderScreen("/agentcore/payment", { core }); - await waitForText(() => screen.frames.join("\n"), "Usage:"); - expect(screen.frames.join("\n")).not.toContain("payment"); - expectNoCoreCalls(); - }); -}); diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index d44dfc38b1..4d349954bf 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -1,6 +1,32 @@ import { test, expect, describe } from "bun:test"; import { createRootHandler } from "./index"; -import { createSilentLogger, TestCoreClient, TestGlobalConfigAccessor, testIO } from "../testing"; +import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; +import { + compiledRootCommand, + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../testing"; + +const PUBLIC_COMMANDS = [ + "create", + "add", + "export", + "remove", + "dev", + "deploy", + "invoke", + "log", + "traces", + "status", + "build", + "eval", + "feedback", + "config", + "update", +]; +const STANDALONE_COMMANDS = ["harness", "identity", "runtime", "memory", "gateway", "payment"]; describe("createRootHandler", () => { test("builds the agentcore command tree with its subcommands", () => { @@ -10,22 +36,40 @@ describe("createRootHandler", () => { globalConfigAccessor: new TestGlobalConfigAccessor(), }); expect(root.name()).toBe("agentcore"); - expect(root.children().map((c) => c.name())).toEqual([ - "create", - "add", - "export", - "remove", - "dev", - "deploy", - "invoke", - "log", - "traces", - "status", - "build", - "eval", - "feedback", - "config", - "update", - ]); + expect(root.children().map((c) => c.name())).toEqual(PUBLIC_COMMANDS); + }); + + test.each([ + [false, false], + [false, true], + [true, false], + [true, true], + ])( + "registers standalone commands for root flag %s and Gateway mutation flag %s", + (enabled, mutations) => { + const command = compiledRootCommand(undefined, { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-commands": enabled, + "imperative-mutation-commands": mutations, + }); + const names = command.commands.map((child) => child.name()); + expect(names.filter((name) => !STANDALONE_COMMANDS.includes(name))).toEqual(PUBLIC_COMMANDS); + for (const name of STANDALONE_COMMANDS) { + expect(names.includes(name)).toBe(enabled); + expect(new RegExp(`\\n\\s+${name}\\s`).test(command.helpInformation())).toBe(enabled); + } + const add = command.commands.find((child) => child.name() === "add")!; + expect(add.commands.map((child) => child.name())).toEqual( + expect.arrayContaining(["harness", "runtime", "memory", "gateway"]), + ); + }, + ); + + test.each(STANDALONE_COMMANDS)("rejects disabled %s before command dispatch", async (name) => { + const command = compiledRootCommand(); + command.configureOutput({ writeErr: () => {}, writeOut: () => {} }); + await expect(command.parseAsync(["node", "agentcore", name, "--json"])).rejects.toThrow( + `unknown command '${name}' for 'agentcore'`, + ); }); }); From 5a1d258378269bf3799b4ad82519c97e8555b647 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 15:40:19 +0000 Subject: [PATCH 08/11] test(tui): clarify shared and gateway screen render calls --- src/components/PaginatedTablePicker.test.tsx | 34 +++---- src/components/Root.test.tsx | 4 +- .../gateway/gateway.mutations.screen.test.tsx | 19 ++-- src/handlers/gateway/gateway.screen.test.tsx | 89 +++++++++++-------- .../gateway/invoke/invoke.screen.test.tsx | 58 ++++++------ .../gateway/policy/generate.screen.test.tsx | 14 +-- 6 files changed, 114 insertions(+), 104 deletions(-) diff --git a/src/components/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index b0ea23059a..256b1c7dde 100644 --- a/src/components/PaginatedTablePicker.test.tsx +++ b/src/components/PaginatedTablePicker.test.tsx @@ -8,7 +8,7 @@ import { QueryClient } from "@tanstack/react-query"; import stringWidth from "string-width"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -56,7 +56,7 @@ describe("paginated table picker contract", () => { test("retries a failed query", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("access denied")); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "access denied"); expect(r.lastFrame()).toContain("[r] retry"); @@ -80,7 +80,7 @@ describe("paginated table picker contract", () => { if (args[0] === "t2") throw new Error("page unavailable"); return listHarnesses(...args); }; - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -96,7 +96,7 @@ describe("paginated table picker contract", () => { const core = new TestCoreClient(); const pending = Promise.withResolvers(); core.harness.listHarnesses = async () => pending.promise; - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "loading harnesses"); await r.press("escape"); @@ -106,7 +106,7 @@ describe("paginated table picker contract", () => { test("keeps Escape active after a query fails", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("access denied")); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "access denied"); await r.press("escape"); @@ -114,7 +114,7 @@ describe("paginated table picker contract", () => { }); test("distinguishes first-page and later-page empty states", async () => { - const firstPage = renderScreen("/agentcore/harness/list"); + const firstPage = renderImperativeScreen("/agentcore/harness/list"); await waitForText(firstPage.lastFrame, "No harnesses found in this Region."); expect(firstPage.lastFrame()).not.toContain("page 1"); await firstPage.press("escape"); @@ -127,7 +127,7 @@ describe("paginated table picker contract", () => { nextToken: "t2", }); core.harness.setListResponse({ harnesses: [] }, "t2"); - const laterPage = renderScreen("/agentcore/harness/list", { core }); + const laterPage = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(laterPage.lastFrame, "page 1 · more →"); await laterPage.write("l"); @@ -153,7 +153,7 @@ describe("paginated table picker contract", () => { "t2", ); core.harness.setGetResponse(getResponse(first)); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "page 1 · more →"); expect(r.lastFrame()).toContain("[←→/hl] page"); @@ -211,7 +211,7 @@ describe("paginated table picker contract", () => { queries: { retry: false, gcTime: Infinity, staleTime: 0 }, }, }); - const r = renderScreen("/agentcore/harness/list", { core, queryClient }); + const r = renderImperativeScreen("/agentcore/harness/list", { core, queryClient }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -261,7 +261,7 @@ describe("paginated table picker contract", () => { "t2", ); core.harness.setGetResponse(getResponse(first)); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -286,7 +286,7 @@ describe("paginated table picker contract", () => { const core = coreWith([alpha, beta]); core.harness.setListResponse({ harnesses: [alpha, beta], nextToken: "t2" }); core.harness.setGetResponse(getResponse(alpha)); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "beta"); await r.press("down"); @@ -323,7 +323,7 @@ describe("paginated table picker contract", () => { }), ], }); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "orders-beta"); await r.write("/"); @@ -379,7 +379,7 @@ describe("paginated table picker contract", () => { }, "t2", ); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "matching-page-one"); await r.write("/"); @@ -406,7 +406,7 @@ describe("paginated table picker contract", () => { ), nextToken: "t2", }); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "page 1 · more →"); let lines = (r.lastFrame() ?? "").split("\n"); @@ -437,7 +437,7 @@ describe("paginated table picker contract", () => { }), nextToken: "t2", }); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await r.resize(60, 24); await waitFor(() => { @@ -465,7 +465,7 @@ describe("paginated table picker contract", () => { }), ), }); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, suffixes[0]!); for (const width of [100, 80, 60]) { @@ -492,7 +492,7 @@ describe("paginated table picker contract", () => { test("filters against rendered timestamps and raw identifiers", async () => { const core = new TestCoreClient(); core.runtime.setListResponse({ agentRuntimes: [runtime()] }); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "AbCdEf1234"); await r.write("/"); diff --git a/src/components/Root.test.tsx b/src/components/Root.test.tsx index a42fd10c4b..ced018f34b 100644 --- a/src/components/Root.test.tsx +++ b/src/components/Root.test.tsx @@ -5,7 +5,7 @@ import { cleanupScreens, compiledRootCommand, IMPERATIVE_GLOBAL_CONFIG, - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitFor, } from "../testing"; @@ -62,7 +62,7 @@ describe("every command with a screen", () => { test.each(SCREENS.map(([path, command]) => [path.join(" "), path, command] as const))( "%s opens, and esc returns to a menu above it", async (_label, path, command) => { - const r = renderScreen("/" + path.join("/")); + const r = renderImperativeScreen("/" + path.join("/")); // Wide and tall enough that the header never wraps. await r.resize(220, 200); const menus = ancestorMenuHeaders(path, command); diff --git a/src/handlers/gateway/gateway.mutations.screen.test.tsx b/src/handlers/gateway/gateway.mutations.screen.test.tsx index c516e3977b..342c352f21 100644 --- a/src/handlers/gateway/gateway.mutations.screen.test.tsx +++ b/src/handlers/gateway/gateway.mutations.screen.test.tsx @@ -1,10 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { - cleanupScreens, - menuEntries, - renderImperativeScreen as renderScreen, - waitForText, -} from "../../testing"; +import { cleanupScreens, menuEntries, renderImperativeScreen, waitForText } from "../../testing"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; afterEach(cleanupScreens); @@ -12,7 +7,7 @@ const GROUPS = ["gateway", "gateway/target", "gateway/connector", "gateway/rule" describe("Gateway mutation menus", () => { test.each(GROUPS)("%s omits disabled CLI-only mutations", async (group) => { - const screen = renderScreen(`/agentcore/${group}`); + const screen = renderImperativeScreen(`/agentcore/${group}`); await waitForText(screen.lastFrame, "type to choose a command"); const entries = menuEntries(screen.lastFrame()!); expect(entries.cliOnly).toEqual([]); @@ -24,7 +19,7 @@ describe("Gateway mutation menus", () => { }); test.each(GROUPS)("%s preserves enabled CLI-only mutations", async (group) => { - const screen = renderScreen(`/agentcore/${group}`, { + const screen = renderImperativeScreen(`/agentcore/${group}`, { globalConfig: { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": true, @@ -39,7 +34,7 @@ describe("Gateway mutation menus", () => { }); test("disabled create opens project guidance and returns to the Gateway menu", async () => { - const screen = renderScreen("/agentcore/gateway"); + const screen = renderImperativeScreen("/agentcore/gateway"); await waitForText(screen.lastFrame, "type to choose a command"); expect(menuEntries(screen.lastFrame()!).screens[0]).toBe("create"); await screen.press("return"); @@ -58,7 +53,7 @@ describe("Gateway mutation menus", () => { }); test.each([false, true])("direct create route matches flag %s", async (enabled) => { - const screen = renderScreen("/agentcore/gateway/create", { + const screen = renderImperativeScreen("/agentcore/gateway/create", { globalConfig: { ...DEFAULT_GLOBAL_CONFIG, "imperative-mutation-commands": enabled, @@ -85,14 +80,14 @@ describe("Gateway mutation menus", () => { .map((mutation) => `${group}/${mutation}`), ), )("disabled direct route %s cannot expose mutation help", async (path) => { - const screen = renderScreen(`/agentcore/${path}`); + const screen = renderImperativeScreen(`/agentcore/${path}`); await waitForText(() => screen.frames.join("\n"), "Usage:"); expect(screen.frames.join("\n")).not.toContain("this command runs from the command line"); expect(screen.core.gateway.calls).toEqual([]); }); test("project guidance remains scrollable after resizing a small terminal", async () => { - const screen = renderScreen("/agentcore/gateway/create"); + const screen = renderImperativeScreen("/agentcore/gateway/create"); await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); await screen.resize(50, 12); await screen.write("\u001b[6~"); diff --git a/src/handlers/gateway/gateway.screen.test.tsx b/src/handlers/gateway/gateway.screen.test.tsx index 6aae5d648f..e1b0a4afc2 100644 --- a/src/handlers/gateway/gateway.screen.test.tsx +++ b/src/handlers/gateway/gateway.screen.test.tsx @@ -11,7 +11,7 @@ import { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitForText, menuEntries, @@ -112,7 +112,7 @@ function coreWithGateways(items: GatewaySummary[]): TestCoreClient { describe("Gateway menu and list", () => { test("renders the Gateway command menu without calling Core", async () => { - const screen = renderScreen("/agentcore/gateway"); + const screen = renderImperativeScreen("/agentcore/gateway"); await waitForText(screen.lastFrame, "manage AgentCore Gateways"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -124,7 +124,7 @@ describe("Gateway menu and list", () => { test("renders Gateway identity and calls list with exact Core options", async () => { const core = coreWithGateways([gateway()]); - const screen = renderScreen("/agentcore/gateway/list", { + const screen = renderImperativeScreen("/agentcore/gateway/list", { core, endpointUrl: ENDPOINT, }); @@ -148,20 +148,20 @@ describe("Gateway menu and list", () => { const loadingCore = new TestCoreClient(); const pending = Promise.withResolvers(); loadingCore.gateway.listGateways = async () => pending.promise; - const loading = renderScreen("/agentcore/gateway/list", { core: loadingCore }); + const loading = renderImperativeScreen("/agentcore/gateway/list", { core: loadingCore }); await waitForText(loading.lastFrame, "loading Gateways"); await loading.press("escape"); await waitForText(loading.lastFrame, "manage AgentCore Gateways"); loading.unmount(); - const empty = renderScreen("/agentcore/gateway/list"); + const empty = renderImperativeScreen("/agentcore/gateway/list"); await waitForText(empty.lastFrame, "No Gateways found in this Region."); empty.unmount(); const errorCore = new TestCoreClient(); errorCore.gateway.setError(new Error("gateway unavailable")); - const error = renderScreen("/agentcore/gateway/list", { core: errorCore }); + const error = renderImperativeScreen("/agentcore/gateway/list", { core: errorCore }); await waitForText(error.lastFrame, "gateway unavailable"); expect(error.lastFrame()).toContain("[r] retry"); @@ -174,7 +174,7 @@ describe("Gateway menu and list", () => { test("selects a Gateway and renders only read-only hub actions", async () => { const core = coreWithGateways([gateway()]); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen("/agentcore/gateway/list", { + const screen = renderImperativeScreen("/agentcore/gateway/list", { core, endpointUrl: ENDPOINT, }); @@ -203,17 +203,23 @@ describe("Gateway menu and list", () => { core.gateway.setGetResponse( gatewayDetail({ status: "FAILED", statusReasons: ["Role is unavailable", "Target failed"] }), ); - const screen = renderScreen(`/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, { - core, - }); + const screen = renderImperativeScreen( + `/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, + { + core, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); expect(screen.lastFrame()).toMatch(/statusReasons\s+Role is unavailable; Target failed/); screen.unmount(); core.gateway.setGetResponse(gatewayDetail({ statusReasons: [] })); - const ready = renderScreen(`/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, { - core, - }); + const ready = renderImperativeScreen( + `/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, + { + core, + }, + ); await waitForText(ready.lastFrame, "show the full JSON definition"); expect(ready.lastFrame()).not.toContain("statusReasons"); }); @@ -221,9 +227,12 @@ describe("Gateway menu and list", () => { test("opens complete Gateway JSON from the detail action", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(`/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, { - core, - }); + const screen = renderImperativeScreen( + `/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}`, + { + core, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); await screen.press("return"); @@ -235,7 +244,7 @@ describe("Gateway menu and list", () => { test("bare Gateway get redirects to the Gateway picker", async () => { const core = coreWithGateways([gateway({ name: "redirected-gateway" })]); - const screen = renderScreen("/agentcore/gateway/get", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/get", { core }); await waitForText(screen.lastFrame, "redirected-gateway"); expect(core.gateway.calls[0]?.method).toBe("listGateways"); @@ -244,7 +253,7 @@ describe("Gateway menu and list", () => { describe("Gateway Target flow", () => { test("renders the Target command menu without calling Core", async () => { - const screen = renderScreen("/agentcore/gateway/target"); + const screen = renderImperativeScreen("/agentcore/gateway/target"); await waitForText(screen.lastFrame, "manage Targets for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -259,7 +268,7 @@ describe("Gateway Target flow", () => { core.gateway.setListTargetsResponse({ items: [target(TARGET_ID, "orders-target", TargetType.PASSTHROUGH)], }); - const screen = renderScreen("/agentcore/gateway/target/list", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/target/list", { core }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -283,7 +292,7 @@ describe("Gateway Target flow", () => { items: [target(TARGET_ID, "orders-target", TargetType.PASSTHROUGH)], }) .setGetTargetResponse(targetDetail(TARGET_ID)); - const screen = renderScreen( + const screen = renderImperativeScreen( `/agentcore/gateway/target/list/${encodeURIComponent(GATEWAY_ID)}`, { core, @@ -315,7 +324,7 @@ describe("Gateway Target flow", () => { test("bare Target get redirects to Gateway selection", async () => { const core = coreWithGateways([gateway({ name: "target-parent" })]); - const screen = renderScreen("/agentcore/gateway/target/get", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/target/get", { core }); await waitForText(screen.lastFrame, "target-parent"); expect(core.gateway.calls[0]?.method).toBe("listGateways"); @@ -328,7 +337,7 @@ describe("Gateway Target flow", () => { items: [target(TARGET_ID, "orders-target", TargetType.PASSTHROUGH)], }) .setGetTargetResponse(targetDetail(TARGET_ID)); - const screen = renderScreen("/agentcore/gateway/target/list", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/target/list", { core }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -347,7 +356,7 @@ describe("Gateway Target flow", () => { describe("Gateway Connector flow", () => { test("renders the separate Connector command menu without calling Core", async () => { - const screen = renderScreen("/agentcore/gateway/connector"); + const screen = renderImperativeScreen("/agentcore/gateway/connector"); await waitForText(screen.lastFrame, "manage connectors configured for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -362,7 +371,7 @@ describe("Gateway Connector flow", () => { core.gateway.setListConnectorsResponse({ items: [target(CONNECTOR_ID, "search-connector", TargetType.CONNECTOR)], }); - const screen = renderScreen("/agentcore/gateway/connector/list", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/connector/list", { core }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -381,7 +390,7 @@ describe("Gateway Connector flow", () => { items: [target(CONNECTOR_ID, "search-connector", TargetType.CONNECTOR)], }) .setGetConnectorResponse(targetDetail(CONNECTOR_ID, true)); - const screen = renderScreen( + const screen = renderImperativeScreen( `/agentcore/gateway/connector/list/${encodeURIComponent(GATEWAY_ID)}`, { core }, ); @@ -404,7 +413,7 @@ describe("Gateway Connector flow", () => { test("rejects a non-Connector Target opened through the Connector route", async () => { const core = new TestCoreClient(); core.gateway.setError(new Error(`Gateway Target "${TARGET_ID}" is not connector-backed`)); - const screen = renderScreen( + const screen = renderImperativeScreen( `/agentcore/gateway/connector/get/${encodeURIComponent(GATEWAY_ID)}/${encodeURIComponent(TARGET_ID)}`, { core }, ); @@ -414,7 +423,7 @@ describe("Gateway Connector flow", () => { }); test("shows the Gateway-level empty state when no Connectors exist", async () => { - const screen = renderScreen( + const screen = renderImperativeScreen( `/agentcore/gateway/connector/list/${encodeURIComponent(GATEWAY_ID)}`, ); @@ -424,7 +433,7 @@ describe("Gateway Connector flow", () => { test("bare Connector get redirects to Gateway selection", async () => { const core = coreWithGateways([gateway({ name: "connector-parent" })]); - const screen = renderScreen("/agentcore/gateway/connector/get", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/connector/get", { core }); await waitForText(screen.lastFrame, "connector-parent"); expect(core.gateway.calls[0]?.method).toBe("listGateways"); @@ -433,7 +442,7 @@ describe("Gateway Connector flow", () => { describe("Gateway Rule flow", () => { test("renders the Rule command menu without calling Core", async () => { - const screen = renderScreen("/agentcore/gateway/rule"); + const screen = renderImperativeScreen("/agentcore/gateway/rule"); await waitForText(screen.lastFrame, "manage Rules for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -446,7 +455,7 @@ describe("Gateway Rule flow", () => { test("selects a Gateway before listing Rules", async () => { const core = coreWithGateways([gateway()]); core.gateway.setListRulesResponse({ gatewayRules: [rule()] }); - const screen = renderScreen("/agentcore/gateway/rule/list", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/rule/list", { core }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -466,9 +475,12 @@ describe("Gateway Rule flow", () => { test("opens the selected Rule JSON with exact selectors", async () => { const core = new TestCoreClient(); core.gateway.setListRulesResponse({ gatewayRules: [rule()] }).setGetRuleResponse(ruleDetail()); - const screen = renderScreen(`/agentcore/gateway/rule/list/${encodeURIComponent(GATEWAY_ID)}`, { - core, - }); + const screen = renderImperativeScreen( + `/agentcore/gateway/rule/list/${encodeURIComponent(GATEWAY_ID)}`, + { + core, + }, + ); await waitForText(screen.lastFrame, "Route orders"); await screen.press("return"); @@ -486,9 +498,12 @@ describe("Gateway Rule flow", () => { test("keeps scoped lists empty and retryable", async () => { const core = new TestCoreClient(); core.gateway.setError(new Error("rules unavailable")); - const screen = renderScreen(`/agentcore/gateway/rule/list/${encodeURIComponent(GATEWAY_ID)}`, { - core, - }); + const screen = renderImperativeScreen( + `/agentcore/gateway/rule/list/${encodeURIComponent(GATEWAY_ID)}`, + { + core, + }, + ); await waitForText(screen.lastFrame, "rules unavailable"); expect(screen.lastFrame()).toContain("[r] retry"); @@ -500,7 +515,7 @@ describe("Gateway Rule flow", () => { test("bare Rule get redirects to Gateway selection", async () => { const core = coreWithGateways([gateway({ name: "rule-parent" })]); - const screen = renderScreen("/agentcore/gateway/rule/get", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/rule/get", { core }); await waitForText(screen.lastFrame, "rule-parent"); expect(core.gateway.calls[0]?.method).toBe("listGateways"); diff --git a/src/handlers/gateway/invoke/invoke.screen.test.tsx b/src/handlers/gateway/invoke/invoke.screen.test.tsx index ce00d93626..6065e4c9e2 100644 --- a/src/handlers/gateway/invoke/invoke.screen.test.tsx +++ b/src/handlers/gateway/invoke/invoke.screen.test.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { GatewaySummary, GetGatewayResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -62,7 +62,7 @@ describe("Gateway invoke routing", () => { test("selects a Gateway before opening the JSON console", async () => { const core = new TestCoreClient(); core.gateway.setListResponse({ items: [gatewaySummary()] }).setGetResponse(gatewayDetail()); - const screen = renderScreen("/agentcore/gateway/invoke", { core }); + const screen = renderImperativeScreen("/agentcore/gateway/invoke", { core }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -82,7 +82,7 @@ describe("Gateway invoke routing", () => { name: "AccessDeniedException", }); }; - const errorScreen = renderScreen(CONSOLE_PATH, { core }); + const errorScreen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(errorScreen.lastFrame, "AccessDeniedException"); await waitForText(errorScreen.lastFrame, "not authorized for this Gateway"); @@ -94,7 +94,7 @@ describe("Gateway invoke routing", () => { signal = nextSignal; return new Promise(() => {}); }; - const pendingScreen = renderScreen(CONSOLE_PATH, { core: pendingCore }); + const pendingScreen = renderImperativeScreen(CONSOLE_PATH, { core: pendingCore }); await waitFor(() => signal !== undefined && !signal.aborted); pendingScreen.unmount(); await waitFor(() => signal!.aborted); @@ -103,7 +103,7 @@ describe("Gateway invoke routing", () => { test("idle Escape returns through the invoke picker to the Gateway menu", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()).setListResponse({ items: [gatewaySummary()] }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.press("escape"); @@ -124,7 +124,7 @@ describe("Gateway invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Enter JSON payload"); const sessionId = displayedSessionId(screen.lastFrame()); @@ -155,7 +155,7 @@ describe("Gateway invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, `Auth: ${authorizerType}`); await screen.write("{}"); @@ -170,7 +170,7 @@ describe("Gateway invoke JSON console", () => { test("rejects invalid JSON without clearing the editor or invoking", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write('{"prompt":'); @@ -184,7 +184,7 @@ describe("Gateway invoke JSON console", () => { test("keeps the draft out of history when request normalization fails", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -211,7 +211,7 @@ describe("Gateway invoke JSON console", () => { contentType: "application/json", body: responseBody(Buffer.from('{"ok":true}')), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -247,7 +247,7 @@ describe("Gateway invoke JSON console", () => { test("proactively blocks CUSTOM_JWT submission without a bearer token", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail({ authorizerType: "CUSTOM_JWT" })); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "CUSTOM_JWT Gateway requires --bearer-token"); await screen.write("{}"); @@ -262,7 +262,7 @@ describe("Gateway invoke JSON console", () => { test("blocks a non-READY Gateway with its current status", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail({ status: "FAILED" })); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Gateway is FAILED; invocation requires READY"); await screen.write("{}"); @@ -278,7 +278,7 @@ describe("Gateway invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); const initialSession = displayedSessionId(screen.lastFrame()); @@ -300,7 +300,7 @@ describe("Gateway invoke JSON console", () => { const seededPath = "runtime/invocations"; const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -333,7 +333,7 @@ describe("Gateway invoke JSON console", () => { mcpProtocolVersion: "2025-06-18", body: responseBody(Buffer.from("old response")), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -383,7 +383,7 @@ describe("Gateway invoke JSON console", () => { mcpSessionId: "returned-mcp", body: responseBody(Buffer.from("old response")), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -427,7 +427,7 @@ describe("Gateway invoke JSON console", () => { core.gateway .setGetResponse(gatewayDetail({ authorizerType: "CUSTOM_JWT" })) .setListResponse({ items: [gatewaySummary()] }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(GatewayInvokeLaunchContextKey, { @@ -466,7 +466,7 @@ describe("Gateway invoke JSON console", () => { yield Buffer.from("data: second\n"); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); const initialSession = displayedSessionId(screen.lastFrame()); @@ -509,7 +509,7 @@ describe("Gateway invoke JSON console", () => { mcpProtocolVersion: "2025-06-18", body: responseBody(Buffer.from('{"message":"invalid"}')), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -543,7 +543,7 @@ describe("Gateway invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("Temporary Redirect")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -564,7 +564,7 @@ describe("Gateway invoke JSON console", () => { contentType: "application/json", body: responseBody(Buffer.from(raw)), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -596,7 +596,7 @@ describe("Gateway invoke JSON console", () => { contentType, body: responseBody(bytes), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -623,7 +623,7 @@ describe("Gateway invoke JSON console", () => { }); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -644,7 +644,7 @@ describe("Gateway invoke JSON console", () => { contentType: "", body: responseBody(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -667,7 +667,7 @@ describe("Gateway invoke JSON console", () => { yield Buffer.from([0, 255]); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -708,7 +708,7 @@ describe("Gateway invoke JSON console", () => { })(), }; }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); const initialSession = displayedSessionId(screen.lastFrame()); @@ -731,7 +731,7 @@ describe("Gateway invoke JSON console", () => { test("keeps status and shortcuts stable at narrow terminal widths", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.resize(80, 24); @@ -746,7 +746,7 @@ describe("Gateway invoke JSON console", () => { test("horizontally windows long single-line JSON without corrupting status rows", async () => { const core = new TestCoreClient(); core.gateway.setGetResponse(gatewayDetail()); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await screen.resize(100, 24); await waitForText(screen.lastFrame, "Ready"); @@ -772,7 +772,7 @@ describe("Gateway invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from(response)), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await screen.resize(80, 16); await waitForText(screen.lastFrame, "Ready"); diff --git a/src/handlers/gateway/policy/generate.screen.test.tsx b/src/handlers/gateway/policy/generate.screen.test.tsx index 7b8fb7b11d..d3562df762 100644 --- a/src/handlers/gateway/policy/generate.screen.test.tsx +++ b/src/handlers/gateway/policy/generate.screen.test.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { GatewaySummary, GetGatewayResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitForText, } from "../../../testing"; @@ -38,7 +38,7 @@ function coreWith(engineArn: string | undefined): TestCoreClient { describe("gateway policy generate screen", () => { test("picks a gateway, then shows its engine and the prompt", async () => { - const screen = renderScreen("/agentcore/gateway/policy/generate", { + const screen = renderImperativeScreen("/agentcore/gateway/policy/generate", { core: coreWith(ENGINE_ARN), }); @@ -53,7 +53,7 @@ describe("gateway policy generate screen", () => { }); test("escape returns the form through the picker to the Gateway menu", async () => { - const screen = renderScreen("/agentcore/gateway/policy/generate", { + const screen = renderImperativeScreen("/agentcore/gateway/policy/generate", { core: coreWith(ENGINE_ARN), }); @@ -69,7 +69,7 @@ describe("gateway policy generate screen", () => { }); test("explains when the gateway has no engine and offers no prompt", async () => { - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + const screen = renderImperativeScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core: coreWith(undefined), }); @@ -91,7 +91,7 @@ describe("gateway policy generate screen", () => { { statement: PERMIT, findings: [] }, ], }; - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + const screen = renderImperativeScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core, }); await waitForText(screen.lastFrame, PLACEHOLDER); @@ -120,7 +120,7 @@ describe("gateway policy generate screen", () => { test("aborts the run and returns to the picker on esc while generating", async () => { const core = coreWith(ENGINE_ARN); core.policy.hang = true; - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + const screen = renderImperativeScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core, }); await waitForText(screen.lastFrame, PLACEHOLDER); @@ -137,7 +137,7 @@ describe("gateway policy generate screen", () => { test("shows the error and returns to the form on esc", async () => { const core = coreWith(ENGINE_ARN); core.policy.error = new Error("policy generation 'gen-1' failed: bad prompt"); - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { + const screen = renderImperativeScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core, }); await waitForText(screen.lastFrame, PLACEHOLDER); From 8ab2fba1a33d2cf04b599ae885568f3f7a53515e Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 15:40:50 +0000 Subject: [PATCH 09/11] test(harness): use explicit imperative renderer names --- .../harness/create/create.screen.test.tsx | 24 +++++------ .../harness/delete/delete.screen.test.tsx | 14 +++---- .../endpoint/create/create.screen.test.tsx | 8 ++-- .../endpoint/delete/delete.screen.test.tsx | 8 ++-- .../endpoint/list/list.screen.test.tsx | 14 +++---- .../endpoint/update/update.screen.test.tsx | 8 ++-- .../harness/exec/exec.screen.test.tsx | 22 +++++----- src/handlers/harness/get/get.screen.test.tsx | 18 ++++---- .../harness/invoke/invoke.screen.test.tsx | 42 +++++++++---------- .../harness/list/list.screen.test.tsx | 6 +-- .../harness/update/update.screen.test.tsx | 18 ++++---- .../harness/version/list/list.screen.test.tsx | 18 ++++---- 12 files changed, 100 insertions(+), 100 deletions(-) diff --git a/src/handlers/harness/create/create.screen.test.tsx b/src/handlers/harness/create/create.screen.test.tsx index 9d03fcd8bf..e4e07a3283 100644 --- a/src/handlers/harness/create/create.screen.test.tsx +++ b/src/handlers/harness/create/create.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { CreateHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -38,7 +38,7 @@ function coreForCreate(): TestCoreClient { describe("harness create wizard", () => { test("walks name → model → memory → tools → prompt → review and creates", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); // Step: name. await waitForText(r.lastFrame, "the name of your harness"); @@ -120,7 +120,7 @@ describe("harness create wizard", () => { }); test("reveals model fields only after enter and hides them again on escape", async () => { - const r = renderScreen("/agentcore/harness/create", { core: coreForCreate() }); + const r = renderImperativeScreen("/agentcore/harness/create", { core: coreForCreate() }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -142,7 +142,7 @@ describe("harness create wizard", () => { test("selecting gemini collects the model id and api key arn", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -185,7 +185,7 @@ describe("harness create wizard", () => { test("openai requires the model id and api key arn and sends them", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -233,7 +233,7 @@ describe("harness create wizard", () => { test("litellm omits the optional fields left empty", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -273,7 +273,7 @@ describe("harness create wizard", () => { test("service default sends no model", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -304,7 +304,7 @@ describe("harness create wizard", () => { }); test("rejects an invalid name and stays on the name step", async () => { - const r = renderScreen("/agentcore/harness/create", { core: coreForCreate() }); + const r = renderImperativeScreen("/agentcore/harness/create", { core: coreForCreate() }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("9bad name"); @@ -318,7 +318,7 @@ describe("harness create wizard", () => { test("bring-your-own memory requires an ARN", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -364,7 +364,7 @@ describe("harness create wizard", () => { core.harness.createHarness = async () => { throw new Error("name already exists"); }; - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); await waitForText(r.lastFrame, "the name of your harness"); await r.write("my_agent"); @@ -397,7 +397,7 @@ describe("harness create wizard", () => { status: "READY", }, } as never); - const r = renderScreen("/agentcore/harness/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/create", { core }); // Fastest path through the wizard: defaults everywhere. await waitForText(r.lastFrame, "the name of your harness"); @@ -431,7 +431,7 @@ describe("harness create wizard", () => { const core = coreForCreate(); core.harness.setListResponse({ harnesses: [] }); // Arrive from the harness menu so esc has somewhere to pop back to. - const r = renderScreen("/agentcore/harness", { core }); + const r = renderImperativeScreen("/agentcore/harness", { core }); // `create` is the first menu item, so it is already selected. await waitForText(r.lastFrame, "❯ create"); await r.press("return"); diff --git a/src/handlers/harness/delete/delete.screen.test.tsx b/src/handlers/harness/delete/delete.screen.test.tsx index 4b01964feb..dc8b04daa4 100644 --- a/src/handlers/harness/delete/delete.screen.test.tsx +++ b/src/handlers/harness/delete/delete.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -50,7 +50,7 @@ function coreWithHarness(): TestCoreClient { describe("harness delete screen", () => { test("without a harness id, picking a harness opens its confirmation", async () => { const core = coreWithHarness(); - const r = renderScreen("/agentcore/harness/delete", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete", { core }); await waitForText(r.lastFrame, "MyHarness"); expect(r.lastFrame()).toContain("choose a harness to delete"); @@ -62,7 +62,7 @@ describe("harness delete screen", () => { test("shows the harness summary and a default-No confirmation", async () => { const core = coreWithHarness(); - const r = renderScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "Delete harness MyHarness?"); const frame = r.lastFrame()!; @@ -74,7 +74,7 @@ describe("harness delete screen", () => { test("`y` calls DeleteHarness and shows the result", async () => { const core = coreWithHarness(); - const r = renderScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "Delete harness MyHarness?"); await r.write("y"); @@ -88,7 +88,7 @@ describe("harness delete screen", () => { test("enter after success returns to the harness list", async () => { const core = coreWithHarness(); - const r = renderScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "Delete harness MyHarness?"); await r.write("y"); @@ -100,7 +100,7 @@ describe("harness delete screen", () => { test("`n` cancels without calling DeleteHarness", async () => { const core = coreWithHarness(); - const r = renderScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "Delete harness MyHarness?"); await r.write("n"); @@ -115,7 +115,7 @@ describe("harness delete screen", () => { core.harness.deleteHarness = async () => { throw new Error("delete conflict"); }; - const r = renderScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/delete/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "Delete harness MyHarness?"); await r.write("y"); diff --git a/src/handlers/harness/endpoint/create/create.screen.test.tsx b/src/handlers/harness/endpoint/create/create.screen.test.tsx index 83e1309f06..c6b8eb35ea 100644 --- a/src/handlers/harness/endpoint/create/create.screen.test.tsx +++ b/src/handlers/harness/endpoint/create/create.screen.test.tsx @@ -4,7 +4,7 @@ import type { HarnessVersionSummary, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -62,7 +62,7 @@ function coreForCreate(): TestCoreClient { describe("harness endpoint create wizard", () => { test("without a harness id, picking a harness opens the wizard", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/endpoint/create", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/create", { core }); await waitForText(r.lastFrame, "MyHarness"); expect(r.lastFrame()).toContain("choose a harness to create an endpoint for"); @@ -74,7 +74,7 @@ describe("harness endpoint create wizard", () => { test("walks name → version → review and creates", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { core, }); @@ -123,7 +123,7 @@ describe("harness endpoint create wizard", () => { test("keeping `latest` omits targetVersion from the request", async () => { const core = coreForCreate(); - const r = renderScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/create/MyHarness-abc123", { core, }); diff --git a/src/handlers/harness/endpoint/delete/delete.screen.test.tsx b/src/handlers/harness/endpoint/delete/delete.screen.test.tsx index 15286b79ba..c700d79e6d 100644 --- a/src/handlers/harness/endpoint/delete/delete.screen.test.tsx +++ b/src/handlers/harness/endpoint/delete/delete.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -53,7 +53,7 @@ function coreWithEndpoint(): TestCoreClient { describe("harness endpoint delete screen", () => { test("walks from harness picker to endpoint picker to confirmation", async () => { const core = coreWithEndpoint(); - const r = renderScreen("/agentcore/harness/endpoint/delete", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/delete", { core }); await waitForText(r.lastFrame, "MyHarness"); await r.press("return"); @@ -66,7 +66,7 @@ describe("harness endpoint delete screen", () => { test("`y` calls DeleteHarnessEndpoint and shows the result", async () => { const core = coreWithEndpoint(); - const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { core, }); @@ -82,7 +82,7 @@ describe("harness endpoint delete screen", () => { test("`n` cancels without calling DeleteHarnessEndpoint", async () => { const core = coreWithEndpoint(); - const r = renderScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/delete/MyHarness-abc123/prod", { core, }); diff --git a/src/handlers/harness/endpoint/list/list.screen.test.tsx b/src/handlers/harness/endpoint/list/list.screen.test.tsx index a2d41f3017..2fd5744b7d 100644 --- a/src/handlers/harness/endpoint/list/list.screen.test.tsx +++ b/src/handlers/harness/endpoint/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -50,7 +50,7 @@ function coreWithEndpoints(endpoints: HarnessEndpoint[]): TestCoreClient { describe("harness endpoint list screen", () => { test("without a harness id, picking a harness lists its endpoints", async () => { const core = coreWithEndpoints([endpoint()]); - const r = renderScreen("/agentcore/harness/endpoint/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/list", { core }); // Harness picker first. await waitForText(r.lastFrame, "MyHarness"); @@ -65,7 +65,7 @@ describe("harness endpoint list screen", () => { test("makes one exact scoped endpoint list call", async () => { const core = coreWithEndpoints([endpoint()]); - const r = renderScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); await waitFor(() => core.harness.calls.some((call) => call.method === "listHarnessEndpoints")); expect(core.harness.calls.filter((call) => call.method === "listHarnessEndpoints")).toEqual([ @@ -95,7 +95,7 @@ describe("harness endpoint list screen", () => { updatedAt: new Date("2026-07-18T02:00:00.000Z"), }), ]); - const r = renderScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "visible-endpoint"); const frame = r.lastFrame()!; @@ -111,7 +111,7 @@ describe("harness endpoint list screen", () => { test("uses harness-specific first-page wording", async () => { const core = coreWithEndpoints([]); - const r = renderScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "This harness has no endpoints."); expect(r.lastFrame()).not.toContain("No endpoints on this page"); @@ -121,7 +121,7 @@ describe("harness endpoint list screen", () => { test("enter on a row opens the endpoint's JSON detail", async () => { const core = coreWithEndpoints([endpoint()]); core.harness.setGetEndpointResponse({ endpoint: endpoint() }); - const r = renderScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "prod"); await r.press("return"); @@ -136,7 +136,7 @@ describe("harness endpoint list screen", () => { test("retries a failed endpoint detail without losing its selectors", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("endpoint unavailable")); - const r = renderScreen("/agentcore/harness/endpoint/get/MyHarness-abc123/prod", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/get/MyHarness-abc123/prod", { core, }); await waitForText(r.lastFrame, "endpoint unavailable"); diff --git a/src/handlers/harness/endpoint/update/update.screen.test.tsx b/src/handlers/harness/endpoint/update/update.screen.test.tsx index 263dbeaada..9fcde20105 100644 --- a/src/handlers/harness/endpoint/update/update.screen.test.tsx +++ b/src/handlers/harness/endpoint/update/update.screen.test.tsx @@ -4,7 +4,7 @@ import type { HarnessVersionSummary, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -72,7 +72,7 @@ function coreForUpdate(): TestCoreClient { describe("harness endpoint update wizard", () => { test("walks harness picker → endpoint picker → wizard", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/endpoint/update", { core }); + const r = renderImperativeScreen("/agentcore/harness/endpoint/update", { core }); await waitForText(r.lastFrame, "MyHarness"); await r.press("return"); @@ -85,7 +85,7 @@ describe("harness endpoint update wizard", () => { test("repointing at a new version submits only targetVersion", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { core, }); @@ -113,7 +113,7 @@ describe("harness endpoint update wizard", () => { test("keeping the version unchanged submits only the endpoint identity", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { + const r = renderImperativeScreen("/agentcore/harness/endpoint/update/MyHarness-abc123/prod", { core, }); diff --git a/src/handlers/harness/exec/exec.screen.test.tsx b/src/handlers/harness/exec/exec.screen.test.tsx index e2a3eb3fe2..94bed2947c 100644 --- a/src/handlers/harness/exec/exec.screen.test.tsx +++ b/src/handlers/harness/exec/exec.screen.test.tsx @@ -6,7 +6,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse, HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, cleanupScreens, StreamController, @@ -57,7 +57,7 @@ function execCore(): TestCoreClient { return core; } -async function type(r: ReturnType, text: string) { +async function type(r: ReturnType, text: string) { await r.write(text); await r.press("return"); } @@ -66,7 +66,7 @@ describe("exec screen", () => { test("the exec route opens a picker, and selection lands in exec mode", async () => { const core = execCore(); core.harness.setListResponse({ harnesses: [summary()] }); - const r = renderScreen("/agentcore/harness/exec", { core }); + const r = renderImperativeScreen("/agentcore/harness/exec", { core }); await waitForText(r.lastFrame, "choose a harness to exec into"); await waitForText(r.lastFrame, "MyHarness"); @@ -79,7 +79,7 @@ describe("exec screen", () => { test("enter runs the command in the chat session's container and shows output inline", async () => { const core = execCore(); - const r = renderScreen(EXEC_PATH, { core }); + const r = renderImperativeScreen(EXEC_PATH, { core }); await waitForText(r.lastFrame, "run a command…"); await type(r, "ls /"); @@ -103,7 +103,7 @@ describe("exec screen", () => { test("a session id in the route resumes that session", async () => { const resumed = "resumed-session-0123456789abcdefghijklmn"; // 33+ chars const core = execCore(); - const r = renderScreen(`${EXEC_PATH}/${resumed}`, { core }); + const r = renderImperativeScreen(`${EXEC_PATH}/${resumed}`, { core }); await waitForText(r.lastFrame, `session: ${resumed}`); await type(r, "pwd"); @@ -120,7 +120,7 @@ describe("exec screen", () => { { chunk: { contentDelta: { stderr: "ls: cannot access '/nope'\n" } } }, { chunk: { contentStop: { exitCode: 2, status: "COMPLETED" } } }, ); - const r = renderScreen(EXEC_PATH, { core }); + const r = renderImperativeScreen(EXEC_PATH, { core }); await waitForText(r.lastFrame, "run a command…"); await type(r, "ls /nope"); @@ -131,7 +131,7 @@ describe("exec screen", () => { }); test("ctrl+e flips between exec and chat mode", async () => { - const r = renderScreen(EXEC_PATH, { core: execCore() }); + const r = renderImperativeScreen(EXEC_PATH, { core: execCore() }); await waitForText(r.lastFrame, "run a command…"); expect(r.lastFrame()).toContain("[ctrl+e] chat mode"); @@ -148,7 +148,7 @@ describe("exec screen", () => { test("chat turns and exec commands share one session and one transcript", async () => { const core = execCore(); // Start on the invoke route (chat mode), then toggle into exec mode. - const r = renderScreen("/agentcore/harness/invoke/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "send a message…"); await type(r, "hi agent"); @@ -177,7 +177,7 @@ describe("exec screen", () => { const core = execCore(); const stream = new StreamController(); core.harness.queueExecStream(stream); - const r = renderScreen(EXEC_PATH, { core }); + const r = renderImperativeScreen(EXEC_PATH, { core }); await waitForText(r.lastFrame, "run a command…"); await type(r, "sleep 999"); @@ -194,7 +194,7 @@ describe("exec screen", () => { test("an exec failure renders a ✗ error item and recovers to idle", async () => { const core = execCore(); - const r = renderScreen(EXEC_PATH, { core }); + const r = renderImperativeScreen(EXEC_PATH, { core }); await waitForText(r.lastFrame, "run a command…"); core.harness.setError(new Error("runtime unreachable")); @@ -207,7 +207,7 @@ describe("exec screen", () => { test("empty input in exec mode does not run anything", async () => { const core = execCore(); - const r = renderScreen(EXEC_PATH, { core }); + const r = renderImperativeScreen(EXEC_PATH, { core }); await waitForText(r.lastFrame, "run a command…"); await r.press("return"); diff --git a/src/handlers/harness/get/get.screen.test.tsx b/src/handlers/harness/get/get.screen.test.tsx index 794ef5dc04..58479cab8f 100644 --- a/src/handlers/harness/get/get.screen.test.tsx +++ b/src/handlers/harness/get/get.screen.test.tsx @@ -9,7 +9,7 @@ import type { Harness, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -45,7 +45,7 @@ function getResponse(): GetHarnessResponse { function hubScreen() { const core = new TestCoreClient(); core.harness.setGetResponse(getResponse()); - return { core, r: renderScreen("/agentcore/harness/get/MyHarness-abc123", { core }) }; + return { core, r: renderImperativeScreen("/agentcore/harness/get/MyHarness-abc123", { core }) }; } describe("harness hub screen", () => { @@ -73,7 +73,7 @@ describe("harness hub screen", () => { failureReason: "Execution role is unavailable", }, }); - const r = renderScreen("/agentcore/harness/get/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/get/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "show the full JSON definition"); expect(r.lastFrame()).toMatch(/failureReason\s+Execution role is unavailable/); }); @@ -103,7 +103,7 @@ describe("harness hub screen", () => { test("shows the error message when the get call fails", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("harness not found")); - const r = renderScreen("/agentcore/harness/get/does-not-exist", { core }); + const r = renderImperativeScreen("/agentcore/harness/get/does-not-exist", { core }); await waitForText(r.lastFrame, "Error:"); expect(r.lastFrame()).toContain("harness not found"); @@ -184,7 +184,7 @@ describe("harness hub screen", () => { }, ], }); - const r = renderScreen("/agentcore/harness/get", { core }); + const r = renderImperativeScreen("/agentcore/harness/get", { core }); // The redirect lands on the list, which fetches harnesses. await waitForText(r.lastFrame, "MyHarness"); @@ -274,7 +274,7 @@ function linkedHubScreen( name: "github-oauth", credentialProviderArn: OAUTH2_ARN, } as GetOauth2CredentialProviderResponse); - return { core, r: renderScreen(path, { core }) }; + return { core, r: renderImperativeScreen(path, { core }) }; } // markedLines returns the lines carrying the ❯ focus marker. @@ -284,7 +284,7 @@ function markedLines(frame: string | undefined): string[] { // The action list has six entries, so five downs reach `update` and the sixth // crosses into the tree. -async function focusTree(r: ReturnType, row = 0) { +async function focusTree(r: ReturnType, row = 0) { for (let press = 0; press < 6 + row; press++) await r.press("down"); } @@ -727,7 +727,7 @@ describe("harness JSON detail screen", () => { const core = new TestCoreClient(); core.harness.setGetResponse(response); - const r = renderScreen("/agentcore/harness/get/MyHarness-abc123/json", { core }); + const r = renderImperativeScreen("/agentcore/harness/get/MyHarness-abc123/json", { core }); await waitForText(r.lastFrame, "WRAP_SENTINEL"); r.unmount(); @@ -736,7 +736,7 @@ describe("harness JSON detail screen", () => { test("renders the harness JSON and scrolls without crashing", async () => { const core = new TestCoreClient(); core.harness.setGetResponse(getResponse()); - const r = renderScreen("/agentcore/harness/get/MyHarness-abc123/json", { core }); + const r = renderImperativeScreen("/agentcore/harness/get/MyHarness-abc123/json", { core }); await waitForText(r.lastFrame, '"harnessName"'); await r.press("down"); diff --git a/src/handlers/harness/invoke/invoke.screen.test.tsx b/src/handlers/harness/invoke/invoke.screen.test.tsx index 665ce587c9..e3714cc435 100644 --- a/src/handlers/harness/invoke/invoke.screen.test.tsx +++ b/src/handlers/harness/invoke/invoke.screen.test.tsx @@ -5,7 +5,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse, HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -57,7 +57,7 @@ function chatCore(): TestCoreClient { } // sendMessage types `text` into the prompt and presses enter. -async function sendMessage(r: ReturnType, text: string) { +async function sendMessage(r: ReturnType, text: string) { await r.write(text); await r.press("return"); } @@ -71,7 +71,7 @@ describe("invoke picker screen", () => { summary({ harnessName: "beta", harnessId: "beta-2" }), ], }); - const r = renderScreen("/agentcore/harness/invoke", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke", { core }); await waitForText(r.lastFrame, "alpha"); expect(r.lastFrame()).toContain("beta"); @@ -82,7 +82,7 @@ describe("invoke picker screen", () => { test("selecting a harness opens its chat", async () => { const core = chatCore(); core.harness.setListResponse({ harnesses: [summary()] }); - const r = renderScreen("/agentcore/harness/invoke", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke", { core }); await waitForText(r.lastFrame, "MyHarness"); await r.press("return"); @@ -95,7 +95,7 @@ describe("invoke picker screen", () => { test("shows the error message when the list call fails", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("access denied")); - const r = renderScreen("/agentcore/harness/invoke", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke", { core }); await waitForText(r.lastFrame, "Error:"); expect(r.lastFrame()).toContain("access denied"); @@ -105,7 +105,7 @@ describe("invoke picker screen", () => { test("esc returns to the harness menu", async () => { const core = new TestCoreClient(); core.harness.setListResponse({ harnesses: [summary()] }); - const r = renderScreen("/agentcore/harness/invoke", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke", { core }); await waitForText(r.lastFrame, "MyHarness"); await r.press("escape"); @@ -116,7 +116,7 @@ describe("invoke picker screen", () => { describe("invoke chat screen", () => { test("sending a message streams the reply and appends a turn summary", async () => { - const r = renderScreen(CHAT_PATH, { core: chatCore() }); + const r = renderImperativeScreen(CHAT_PATH, { core: chatCore() }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "hi agent"); @@ -131,7 +131,7 @@ describe("invoke chat screen", () => { test("the session id is stable across sends and each send carries only the new message", async () => { const core = chatCore(); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "first"); @@ -155,7 +155,7 @@ describe("invoke chat screen", () => { test("a session id in the route resumes that session", async () => { const resumed = "resumed-session-0123456789abcdefghijklmn"; // 33+ chars const core = chatCore(); - const r = renderScreen(`${CHAT_PATH}/${resumed}`, { core }); + const r = renderImperativeScreen(`${CHAT_PATH}/${resumed}`, { core }); // The bottom bar shows the resumed session immediately. await waitForText(r.lastFrame, `session: ${resumed}`); @@ -194,7 +194,7 @@ describe("invoke chat screen", () => { }, ], }); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); expect(r.lastFrame()).toContain("qualifier: DEFAULT"); @@ -227,7 +227,7 @@ describe("invoke chat screen", () => { }, ], }); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await r.write("\x14"); // ctrl+t @@ -240,7 +240,7 @@ describe("invoke chat screen", () => { test("a qualifier in the route targets that endpoint", async () => { const core = chatCore(); - const r = renderScreen(`${CHAT_PATH}?qualifier=canary`, { core }); + const r = renderImperativeScreen(`${CHAT_PATH}?qualifier=canary`, { core }); await waitForText(r.lastFrame, "qualifier: canary"); await sendMessage(r, "hi"); @@ -313,7 +313,7 @@ describe("invoke chat screen", () => { { contentBlockStop: { contentBlockIndex: 0 } }, { messageStop: { stopReason: "end_turn" } }, ); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "weather?"); @@ -334,7 +334,7 @@ describe("invoke chat screen", () => { const core = chatCore(); const stream = new StreamController(); core.harness.queueInvokeStream(stream); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "take your time"); @@ -356,7 +356,7 @@ describe("invoke chat screen", () => { const core = chatCore(); const stream = new StreamController(); core.harness.queueInvokeStream(stream); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "hi"); @@ -376,7 +376,7 @@ describe("invoke chat screen", () => { test("an invoke failure renders a ✗ error item and recovers to idle", async () => { const core = chatCore(); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); // The harness detail is already fetched; only the upcoming invoke fails. @@ -391,7 +391,7 @@ describe("invoke chat screen", () => { test("shows the error screen when the harness detail fails to load", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("harness not found")); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "Error:"); expect(r.lastFrame()).toContain("harness not found"); @@ -401,7 +401,7 @@ describe("invoke chat screen", () => { test("esc while idle pops back to the picker", async () => { const core = chatCore(); core.harness.setListResponse({ harnesses: [summary()] }); - const r = renderScreen("/agentcore/harness/invoke", { core }); + const r = renderImperativeScreen("/agentcore/harness/invoke", { core }); await waitForText(r.lastFrame, "MyHarness"); await r.press("return"); @@ -414,7 +414,7 @@ describe("invoke chat screen", () => { test("submitting an empty prompt does not invoke", async () => { const core = chatCore(); - const r = renderScreen(CHAT_PATH, { core }); + const r = renderImperativeScreen(CHAT_PATH, { core }); await waitForText(r.lastFrame, "send a message…"); await r.press("return"); @@ -429,7 +429,7 @@ describe("invoke chat screen", () => { // Regression: sending clears the input from outside TextInput, which used // to leave its cursor stranded past the end of the (now shorter) value — // backspace then deleted nothing. - const r = renderScreen(CHAT_PATH, { core: chatCore() }); + const r = renderImperativeScreen(CHAT_PATH, { core: chatCore() }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "hi agent"); @@ -445,7 +445,7 @@ describe("invoke chat screen", () => { }); test("arrow keys scroll the transcript without crashing", async () => { - const r = renderScreen(CHAT_PATH, { core: chatCore() }); + const r = renderImperativeScreen(CHAT_PATH, { core: chatCore() }); await waitForText(r.lastFrame, "send a message…"); await sendMessage(r, "hi agent"); diff --git a/src/handlers/harness/list/list.screen.test.tsx b/src/handlers/harness/list/list.screen.test.tsx index f2640a7119..dd3216afed 100644 --- a/src/handlers/harness/list/list.screen.test.tsx +++ b/src/handlers/harness/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -39,7 +39,7 @@ describe("harness list screen", () => { harness({ harnessName: "alpha", harnessId: "alpha-1", harnessVersion: "99999" }), harness({ harnessName: "beta", harnessId: "beta-2" }), ]); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitForText(r.lastFrame, "alpha"); const frame = r.lastFrame()!; @@ -55,7 +55,7 @@ describe("harness list screen", () => { test("makes one initial list request with context options", async () => { const core = coreWith([harness()]); - const r = renderScreen("/agentcore/harness/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/list", { core }); await waitFor(() => core.harness.calls.some((call) => call.method === "listHarnesses")); expect(core.harness.calls.filter((call) => call.method === "listHarnesses")).toEqual([ diff --git a/src/handlers/harness/update/update.screen.test.tsx b/src/handlers/harness/update/update.screen.test.tsx index 503984e312..bae16f9fe6 100644 --- a/src/handlers/harness/update/update.screen.test.tsx +++ b/src/handlers/harness/update/update.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -66,7 +66,7 @@ function coreForUpdate(): TestCoreClient { describe("harness update wizard", () => { test("without a harness id, picking a harness opens its wizard", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update", { core }); + const r = renderImperativeScreen("/agentcore/harness/update", { core }); await waitForText(r.lastFrame, "MyHarness"); expect(r.lastFrame()).toContain("choose a harness to update"); @@ -77,7 +77,7 @@ describe("harness update wizard", () => { test("starts on model (no rename) with values prefilled from the harness", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); // The harness has no bedrock model configured, so keep-current is // preselected; enter leaves the model untouched. @@ -103,7 +103,7 @@ describe("harness update wizard", () => { bedrockModelConfig: { modelId: "us.anthropic.claude-opus-4-8" }, }; core.harness.setGetResponse(current); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); // The harness's bedrock provider is preselected. Its persisted model id is // revealed after confirming the provider. @@ -139,7 +139,7 @@ describe("harness update wizard", () => { agentCoreMemoryConfiguration: { arn: memoryArn }, }; core.harness.setGetResponse(current); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "● keep current"); await r.press("return"); @@ -163,7 +163,7 @@ describe("harness update wizard", () => { test("changing the model submits a request with just that field", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "● keep current"); await r.press("down"); // bedrock @@ -191,7 +191,7 @@ describe("harness update wizard", () => { test("changing only the prompt submits a request with just that field", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "● keep current"); await r.press("return"); // model unchanged @@ -223,7 +223,7 @@ describe("harness update wizard", () => { test("disabling memory sends the wrapped disabled configuration", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "● keep current"); await r.press("return"); // model unchanged @@ -250,7 +250,7 @@ describe("harness update wizard", () => { test("toggling a tool off keeps unmodeled tools in the replacement list", async () => { const core = coreForUpdate(); - const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/update/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "● keep current"); await r.press("return"); // model unchanged diff --git a/src/handlers/harness/version/list/list.screen.test.tsx b/src/handlers/harness/version/list/list.screen.test.tsx index 5f80b7665a..f74621794c 100644 --- a/src/handlers/harness/version/list/list.screen.test.tsx +++ b/src/handlers/harness/version/list/list.screen.test.tsx @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import type { HarnessVersionSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { - renderImperativeScreen as renderScreen, + renderImperativeScreen, waitForText, waitFor, cleanupScreens, @@ -48,7 +48,7 @@ function coreWithVersions(versions: HarnessVersionSummary[]): TestCoreClient { describe("harness version list screen", () => { test("without a harness id, picking a harness lists its versions", async () => { const core = coreWithVersions([version({ harnessVersion: "42" })]); - const r = renderScreen("/agentcore/harness/version/list", { core }); + const r = renderImperativeScreen("/agentcore/harness/version/list", { core }); await waitForText(r.lastFrame, "MyHarness"); expect(r.lastFrame()).toContain("choose a harness to list versions for"); @@ -61,7 +61,7 @@ describe("harness version list screen", () => { test("makes one exact scoped version list call", async () => { const core = coreWithVersions([version()]); - const r = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); await waitFor(() => core.harness.calls.some((call) => call.method === "listHarnessVersions")); expect(core.harness.calls.filter((call) => call.method === "listHarnessVersions")).toEqual([ @@ -86,7 +86,7 @@ describe("harness version list screen", () => { version({ harnessVersion: "2", status: "UPDATE_FAILED" }), version({ harnessVersion: "10", status: "READY" }), ]); - const r = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "UPDATE_FAILED"); const frame = r.lastFrame()!; @@ -109,7 +109,7 @@ describe("harness version list screen", () => { createdAt: new Date("2026-07-18T02:00:00.000Z"), }), ]); - const r = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "UPDATE_FAILED"); const frame = r.lastFrame()!; @@ -123,7 +123,7 @@ describe("harness version list screen", () => { }); test("uses harness-version wording for empty pages", async () => { - const firstPage = renderScreen("/agentcore/harness/version/list/MyHarness-abc123"); + const firstPage = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123"); await waitForText(firstPage.lastFrame, "No versions found."); firstPage.unmount(); @@ -133,7 +133,7 @@ describe("harness version list screen", () => { nextToken: "v2", }); core.harness.setListVersionsResponse({ harnessVersions: [] }, "v2"); - const laterPage = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { + const laterPage = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { core, }); @@ -157,7 +157,7 @@ describe("harness version list screen", () => { status: "READY", }, } as Awaited>); - const r = renderScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); + const r = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { core }); await waitForText(r.lastFrame, "READY"); await r.press("return"); @@ -172,7 +172,7 @@ describe("harness version list screen", () => { test("retries a failed version detail without losing its selectors", async () => { const core = new TestCoreClient(); core.harness.setError(new Error("version unavailable")); - const r = renderScreen("/agentcore/harness/version/get/MyHarness-abc123/42", { + const r = renderImperativeScreen("/agentcore/harness/version/get/MyHarness-abc123/42", { core, }); await waitForText(r.lastFrame, "version unavailable"); From 8b18b85ea25ed8e4f0ada2b1da398243369380b3 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 15:41:20 +0000 Subject: [PATCH 10/11] test(identity): use explicit imperative renderer names --- .../apikey.screen.test.tsx | 58 ++++++++----- .../oauth2.screen.test.tsx | 85 ++++++++++++------- 2 files changed, 88 insertions(+), 55 deletions(-) diff --git a/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx b/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx index 9fc8b4a82c..93f6026eb6 100644 --- a/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx +++ b/src/handlers/identity/api-key-credential-provider/apikey.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, tick, waitFor, @@ -57,7 +57,7 @@ function coreWithProviders(providers: ApiKeyCredentialProviderItem[]): TestCoreC describe("API key credential provider menu", () => { test("lists the read-only commands, then the rest as command line only", async () => { - const screen = renderScreen("/agentcore/identity/api-key-credential-provider"); + const screen = renderImperativeScreen("/agentcore/identity/api-key-credential-provider"); await waitForText(screen.lastFrame, "get an API key credential provider"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -76,7 +76,7 @@ describe("API key credential provider picker", () => { lastUpdatedTime: new Date("2026-07-21T02:03:04.000Z"), }), ]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { core, }); @@ -90,7 +90,7 @@ describe("API key credential provider picker", () => { test("calls listApiKeyCredentialProviders with exact Core options", async () => { const core = coreWithProviders([providerItem()]); - renderScreen("/agentcore/identity/api-key-credential-provider/list", { + renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { core, endpointUrl, }); @@ -110,7 +110,7 @@ describe("API key credential provider picker", () => { test("caps maxResults at the service limit of 20 on a tall terminal", async () => { const core = coreWithProviders([providerItem()]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { core, }); // Terminal taller than the 20-row service cap: page size must still clamp. @@ -126,7 +126,7 @@ describe("API key credential provider picker", () => { }); test("shows first-page and later-page empty states", async () => { - const empty = renderScreen("/agentcore/identity/api-key-credential-provider/list"); + const empty = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list"); await waitForText(empty.lastFrame, "No API key credential providers found in this Region."); empty.unmount(); @@ -136,7 +136,7 @@ describe("API key credential provider picker", () => { nextToken: "page-2", }); core.identity.setListApiKeyResponse({ credentialProviders: [] }, "page-2"); - const paged = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + const paged = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { core, }); @@ -150,7 +150,7 @@ describe("API key credential provider picker", () => { test("bare get redirects to the picker", async () => { const core = coreWithProviders([providerItem({ name: "redirected" })]); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get", { + const screen = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/get", { core, }); @@ -162,7 +162,7 @@ describe("API key credential provider picker", () => { const name = "api key blue"; const core = coreWithProviders([providerItem({ name })]); core.identity.setGetApiKeyResponse(getResponse({ name })); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { core, }); @@ -184,10 +184,13 @@ describe("API key credential provider detail", () => { test("renders a resource summary with only the detail action", async () => { const core = new TestCoreClient(); core.identity.setGetApiKeyResponse(getResponse()); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get/api-key-1", { - core, - endpointUrl, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/api-key-credential-provider/get/api-key-1", + { + core, + endpointUrl, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); const frame = screen.lastFrame()!; @@ -207,9 +210,12 @@ describe("API key credential provider detail", () => { test("opens the complete provider JSON", async () => { const core = new TestCoreClient(); core.identity.setGetApiKeyResponse(getResponse()); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get/api-key-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/api-key-credential-provider/get/api-key-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); await screen.press("return"); @@ -223,9 +229,12 @@ describe("API key credential provider detail", () => { test("retries a failed detail query", async () => { const core = new TestCoreClient(); core.identity.setError(new Error("provider unavailable")); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get/api-key-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/api-key-credential-provider/get/api-key-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "provider unavailable"); expect(screen.lastFrame()).toContain("[r] retry"); @@ -242,10 +251,13 @@ describe("API key credential provider detail", () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } }, }); - const screen = renderScreen("/agentcore/identity/api-key-credential-provider/get/api-key-1", { - core, - queryClient, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/api-key-credential-provider/get/api-key-1", + { + core, + queryClient, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); core.identity.setError(new Error("background refresh failed")); diff --git a/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx b/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx index 09e6ee8df5..4eaf2bb42d 100644 --- a/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx +++ b/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, tick, waitFor, @@ -60,7 +60,7 @@ function coreWithProviders(providers: Oauth2CredentialProviderItem[]): TestCoreC describe("OAuth2 credential provider menu", () => { test("lists the read-only commands, then the rest as command line only", async () => { - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider"); + const screen = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider"); await waitForText(screen.lastFrame, "get an OAuth2 credential provider"); expect(menuEntries(screen.lastFrame()!)).toEqual({ @@ -79,7 +79,7 @@ describe("OAuth2 credential provider picker", () => { lastUpdatedTime: new Date("2026-07-21T02:03:04.000Z"), }), ]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { core, }); @@ -93,7 +93,7 @@ describe("OAuth2 credential provider picker", () => { test("calls listOauth2CredentialProviders with exact Core options", async () => { const core = coreWithProviders([providerItem()]); - renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { core, endpointUrl, }); @@ -113,7 +113,7 @@ describe("OAuth2 credential provider picker", () => { test("caps maxResults at the service limit of 20 on a tall terminal", async () => { const core = coreWithProviders([providerItem()]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { core, }); // Terminal taller than the 20-row service cap: page size must still clamp. @@ -129,7 +129,7 @@ describe("OAuth2 credential provider picker", () => { }); test("shows first-page and later-page empty states", async () => { - const empty = renderScreen("/agentcore/identity/oauth2-credential-provider/list"); + const empty = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list"); await waitForText(empty.lastFrame, "No OAuth2 credential providers found in this Region."); empty.unmount(); @@ -139,7 +139,7 @@ describe("OAuth2 credential provider picker", () => { nextToken: "page-2", }); core.identity.setListOauth2Response({ credentialProviders: [] }, "page-2"); - const paged = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + const paged = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { core, }); @@ -151,7 +151,7 @@ describe("OAuth2 credential provider picker", () => { test("bare get redirects to the picker", async () => { const core = coreWithProviders([providerItem({ name: "redirected" })]); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get", { + const screen = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/get", { core, }); @@ -163,7 +163,7 @@ describe("OAuth2 credential provider picker", () => { const name = "oauth2 blue"; const core = coreWithProviders([providerItem({ name })]); core.identity.setGetOauth2Response(getResponse({ name })); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/list", { + const screen = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { core, }); @@ -185,10 +185,13 @@ describe("OAuth2 credential provider detail", () => { test("renders a resource summary with only the detail action", async () => { const core = new TestCoreClient(); core.identity.setGetOauth2Response(getResponse()); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core, - endpointUrl, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core, + endpointUrl, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); const frame = screen.lastFrame()!; @@ -209,18 +212,24 @@ describe("OAuth2 credential provider detail", () => { test("shows a callback URL only when the service provides one", async () => { const withCallback = new TestCoreClient(); withCallback.identity.setGetOauth2Response(getResponse()); - const shown = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core: withCallback, - }); + const shown = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core: withCallback, + }, + ); await waitForText(shown.lastFrame, "show the full JSON definition"); expect(shown.lastFrame()).toContain("callbackUrl"); shown.unmount(); const noCallback = new TestCoreClient(); noCallback.identity.setGetOauth2Response(getResponse({ callbackUrl: undefined })); - const hidden = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core: noCallback, - }); + const hidden = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core: noCallback, + }, + ); await waitForText(hidden.lastFrame, "show the full JSON definition"); expect(hidden.lastFrame()).not.toContain("callbackUrl"); }); @@ -233,9 +242,12 @@ describe("OAuth2 credential provider detail", () => { failureReason: "authorization server metadata could not be loaded", }), ); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "authorization server metadata could not be loaded"); const frame = screen.lastFrame()!; @@ -246,9 +258,12 @@ describe("OAuth2 credential provider detail", () => { test("opens the complete provider JSON", async () => { const core = new TestCoreClient(); core.identity.setGetOauth2Response(getResponse()); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); await screen.press("return"); @@ -262,9 +277,12 @@ describe("OAuth2 credential provider detail", () => { test("retries a failed detail query", async () => { const core = new TestCoreClient(); core.identity.setError(new Error("provider unavailable")); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "provider unavailable"); expect(screen.lastFrame()).toContain("[r] retry"); @@ -281,10 +299,13 @@ describe("OAuth2 credential provider detail", () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } }, }); - const screen = renderScreen("/agentcore/identity/oauth2-credential-provider/get/oauth2-1", { - core, - queryClient, - }); + const screen = renderImperativeScreen( + "/agentcore/identity/oauth2-credential-provider/get/oauth2-1", + { + core, + queryClient, + }, + ); await waitForText(screen.lastFrame, "show the full JSON definition"); core.identity.setError(new Error("background refresh failed")); From 6026e603f24652af9c29f43f9562a7b3e183a6ad Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 24 Sep 2026 15:41:59 +0000 Subject: [PATCH 11/11] test(cli): clarify runtime and memory screen render calls --- .../memory/actor/actor.screen.test.tsx | 8 +-- .../memory/event/event.screen.test.tsx | 35 +++++++---- src/handlers/memory/memory.screen.test.tsx | 34 +++++------ .../memory/record/record.screen.test.tsx | 46 ++++++++------ .../memory/session/session.screen.test.tsx | 8 +-- .../runtime/endpoint/endpoint.screen.test.tsx | 30 +++++----- .../runtime/invoke/invoke.screen.test.tsx | 60 +++++++++---------- src/handlers/runtime/runtime.screen.test.tsx | 46 +++++++------- .../runtime/shell/shell.screen.test.tsx | 4 +- .../runtime/version/version.screen.test.tsx | 22 +++---- 10 files changed, 155 insertions(+), 138 deletions(-) diff --git a/src/handlers/memory/actor/actor.screen.test.tsx b/src/handlers/memory/actor/actor.screen.test.tsx index 8e87ffa896..233a487062 100644 --- a/src/handlers/memory/actor/actor.screen.test.tsx +++ b/src/handlers/memory/actor/actor.screen.test.tsx @@ -3,7 +3,7 @@ import type { ActorSummary, SessionSummary } from "@aws-sdk/client-bedrock-agent import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -48,7 +48,7 @@ describe("Memory actor list flow", () => { core.memory.setListSessionsResponse({ sessionSummaries: [session({ actorId })], }); - const screen = renderScreen("/agentcore/memory/actor/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/actor/list", { core }); await waitForText(screen.lastFrame, memoryId); await screen.press("return"); @@ -87,13 +87,13 @@ describe("Memory actor list flow", () => { }); test("shows empty and retry states for actor lists", async () => { - const empty = renderScreen("/agentcore/memory/actor/list/memory-1"); + const empty = renderImperativeScreen("/agentcore/memory/actor/list/memory-1"); await waitForText(empty.lastFrame, "No actors found for Memory memory-1."); empty.unmount(); const core = new TestCoreClient(); core.memory.setError(new Error("actors unavailable")); - const failed = renderScreen("/agentcore/memory/actor/list/memory-1", { core }); + const failed = renderImperativeScreen("/agentcore/memory/actor/list/memory-1", { core }); await waitForText(failed.lastFrame, "actors unavailable"); core.memory.setError(undefined); diff --git a/src/handlers/memory/event/event.screen.test.tsx b/src/handlers/memory/event/event.screen.test.tsx index 575b907d7b..ee29f6942e 100644 --- a/src/handlers/memory/event/event.screen.test.tsx +++ b/src/handlers/memory/event/event.screen.test.tsx @@ -3,7 +3,7 @@ import type { ActorSummary, Event, SessionSummary } from "@aws-sdk/client-bedroc import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -67,7 +67,7 @@ describe("Memory event list flow", () => { core.memory.setListEventsResponse({ events: [event({ memoryId, actorId, sessionId })], }); - const screen = renderScreen("/agentcore/memory/event/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/event/list", { core }); await waitForText(screen.lastFrame, memoryId); await screen.press("return"); @@ -112,7 +112,7 @@ describe("Memory event list flow", () => { test("calls listEvents with the exact route scope and Core options", async () => { const core = new TestCoreClient(); core.memory.setListEventsResponse({ events: [event()] }); - renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { + renderImperativeScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { core, endpointUrl: memoryEndpointUrl, }); @@ -149,9 +149,12 @@ describe("Memory event list flow", () => { metadata: { tenant: { stringValue: "acme" } }, }), }); - const screen = renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/memory/event/list/memory-1/actor-1/session-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "event blue"); const frame = screen.lastFrame()!; @@ -184,9 +187,12 @@ describe("Memory event list flow", () => { nextToken: "page-2", }); core.memory.setListEventsResponse({ events: [] }, "page-2"); - const screen = renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/memory/event/list/memory-1/actor-1/session-1", + { + core, + }, + ); await waitForText(screen.lastFrame, "page 1 · more →"); await screen.write("l"); @@ -194,15 +200,18 @@ describe("Memory event list flow", () => { }); test("shows scoped empty and retry states", async () => { - const empty = renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1"); + const empty = renderImperativeScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1"); await waitForText(empty.lastFrame, "No events found for session session-1."); empty.unmount(); const core = new TestCoreClient(); core.memory.setError(new Error("events unavailable")); - const failed = renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { - core, - }); + const failed = renderImperativeScreen( + "/agentcore/memory/event/list/memory-1/actor-1/session-1", + { + core, + }, + ); await waitForText(failed.lastFrame, "events unavailable"); expect(failed.lastFrame()).toContain("[r] retry"); diff --git a/src/handlers/memory/memory.screen.test.tsx b/src/handlers/memory/memory.screen.test.tsx index f38856eaf3..ff2b69efb7 100644 --- a/src/handlers/memory/memory.screen.test.tsx +++ b/src/handlers/memory/memory.screen.test.tsx @@ -8,7 +8,7 @@ import { QueryClient } from "@tanstack/react-query"; import stringWidth from "string-width"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, tick, waitFor, @@ -67,7 +67,7 @@ function coreWithMemories(memories: MemorySummary[]): TestCoreClient { describe("Memory picker", () => { test("shows event, record, actor, and session commands in the Memory TUI menu", async () => { - const screen = renderScreen("/agentcore/memory"); + const screen = renderImperativeScreen("/agentcore/memory"); await waitForText(screen.lastFrame, "inspect AgentCore Memories"); const frame = screen.lastFrame()!; @@ -87,7 +87,7 @@ describe("Memory picker", () => { updatedAt: new Date("2026-07-21T02:03:04.000Z"), }), ]); - const screen = renderScreen("/agentcore/memory/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(screen.lastFrame, "memory-visible-id"); const frame = screen.lastFrame()!; @@ -101,7 +101,7 @@ describe("Memory picker", () => { test("keeps long Memory IDs separate from adjacent columns", async () => { const memoryId = `memory-${"x".repeat(70)}`; const core = coreWithMemories([memorySummary({ id: memoryId })]); - const screen = renderScreen("/agentcore/memory/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(screen.lastFrame, "memory-"); await screen.resize(80, 24); @@ -117,7 +117,7 @@ describe("Memory picker", () => { test("calls listMemories with exact Core options", async () => { const core = coreWithMemories([memorySummary()]); - renderScreen("/agentcore/memory/list", { core, endpointUrl: memoryEndpointUrl }); + renderImperativeScreen("/agentcore/memory/list", { core, endpointUrl: memoryEndpointUrl }); await waitFor(() => core.memory.calls.some((call) => call.method === "listMemories")); expect(core.memory.calls.filter((call) => call.method === "listMemories")).toEqual([ @@ -136,7 +136,7 @@ describe("Memory picker", () => { }); test("shows first-page and later-page empty states", async () => { - const empty = renderScreen("/agentcore/memory/list"); + const empty = renderImperativeScreen("/agentcore/memory/list"); await waitForText(empty.lastFrame, "No Memories found in this Region."); empty.unmount(); @@ -146,7 +146,7 @@ describe("Memory picker", () => { nextToken: "page-2", }); core.memory.setListResponse({ memories: [] }, "page-2"); - const paged = renderScreen("/agentcore/memory/list", { core }); + const paged = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(paged.lastFrame, "page 1 · more →"); await paged.write("l"); @@ -156,7 +156,7 @@ describe("Memory picker", () => { test("bare Memory get redirects to the picker", async () => { const core = coreWithMemories([memorySummary({ id: "redirected-memory" })]); - const screen = renderScreen("/agentcore/memory/get", { core }); + const screen = renderImperativeScreen("/agentcore/memory/get", { core }); await waitForText(screen.lastFrame, "redirected-memory"); expect(core.memory.calls[0]?.method).toBe("listMemories"); @@ -166,7 +166,7 @@ describe("Memory picker", () => { const memoryId = "memory blue"; const core = coreWithMemories([memorySummary({ id: memoryId })]); core.memory.setGetResponse(getMemoryOutput({ id: memoryId })); - const screen = renderScreen("/agentcore/memory/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(screen.lastFrame, memoryId); await screen.press("return"); @@ -181,7 +181,7 @@ describe("Memory detail", () => { test("loads the full view and renders a resource summary", async () => { const core = new TestCoreClient(); core.memory.setGetResponse(getMemoryOutput()); - const screen = renderScreen("/agentcore/memory/get/memory-1", { + const screen = renderImperativeScreen("/agentcore/memory/get/memory-1", { core, endpointUrl: memoryEndpointUrl, }); @@ -210,7 +210,7 @@ describe("Memory detail", () => { test("shows a failure reason only when the service provides one", async () => { const healthyCore = new TestCoreClient(); healthyCore.memory.setGetResponse(getMemoryOutput()); - const healthy = renderScreen("/agentcore/memory/get/memory-1", { core: healthyCore }); + const healthy = renderImperativeScreen("/agentcore/memory/get/memory-1", { core: healthyCore }); await waitForText(healthy.lastFrame, "show the full JSON definition"); expect(healthy.lastFrame()).not.toContain("failureReason"); @@ -220,7 +220,7 @@ describe("Memory detail", () => { failedCore.memory.setGetResponse( getMemoryOutput({ status: "FAILED", failureReason: "Strategy setup failed" }), ); - const failed = renderScreen("/agentcore/memory/get/memory-1", { core: failedCore }); + const failed = renderImperativeScreen("/agentcore/memory/get/memory-1", { core: failedCore }); await waitForText(failed.lastFrame, "Strategy setup failed"); expect(failed.lastFrame()).toContain("failureReason"); @@ -229,7 +229,7 @@ describe("Memory detail", () => { test("opens the complete Memory JSON", async () => { const core = new TestCoreClient(); core.memory.setGetResponse(getMemoryOutput()); - const screen = renderScreen("/agentcore/memory/get/memory-1", { core }); + const screen = renderImperativeScreen("/agentcore/memory/get/memory-1", { core }); await waitForText(screen.lastFrame, "show the full JSON definition"); await screen.press("return"); @@ -244,7 +244,7 @@ describe("Memory detail", () => { core.memory.setListResponse({ memories: [memorySummary()] }); core.memory.setGetResponse(getMemoryOutput()); core.memory.setListActorsResponse({ actorSummaries: [] }); - const screen = renderScreen("/agentcore/memory/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(screen.lastFrame, "memory-1"); await screen.press("return"); @@ -269,7 +269,7 @@ describe("Memory detail", () => { const core = new TestCoreClient(); core.memory.setListResponse({ memories: [memorySummary()] }); core.memory.setGetResponse(getMemoryOutput()); - const screen = renderScreen("/agentcore/memory/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/list", { core }); await waitForText(screen.lastFrame, "memory-1"); await screen.press("return"); @@ -289,7 +289,7 @@ describe("Memory detail", () => { test("retries a failed detail query", async () => { const core = new TestCoreClient(); core.memory.setError(new Error("memory unavailable")); - const screen = renderScreen("/agentcore/memory/get/memory-1", { core }); + const screen = renderImperativeScreen("/agentcore/memory/get/memory-1", { core }); await waitForText(screen.lastFrame, "memory unavailable"); expect(screen.lastFrame()).toContain("[r] retry"); @@ -308,7 +308,7 @@ describe("Memory detail", () => { queries: { retry: false, gcTime: Infinity, staleTime: 0 }, }, }); - const screen = renderScreen("/agentcore/memory/get/memory-1", { core, queryClient }); + const screen = renderImperativeScreen("/agentcore/memory/get/memory-1", { core, queryClient }); await waitForText(screen.lastFrame, "show the full JSON definition"); core.memory.setError(new Error("background refresh failed")); diff --git a/src/handlers/memory/record/record.screen.test.tsx b/src/handlers/memory/record/record.screen.test.tsx index 527acac7a6..512c02c89e 100644 --- a/src/handlers/memory/record/record.screen.test.tsx +++ b/src/handlers/memory/record/record.screen.test.tsx @@ -7,7 +7,7 @@ import type { import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -46,7 +46,7 @@ function recordSummary(overrides: Partial = {}): MemoryReco describe("Memory record list flow", () => { test("renders the record command menu", async () => { - const screen = renderScreen("/agentcore/memory/record"); + const screen = renderImperativeScreen("/agentcore/memory/record"); await waitForText(screen.lastFrame, "inspect AgentCore Memory records"); expect(screen.lastFrame()).toContain("list"); @@ -58,7 +58,7 @@ describe("Memory record list flow", () => { core.memory.setListResponse({ memories: [memorySummary({ id: memoryId })], }); - const screen = renderScreen("/agentcore/memory/record/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/record/list", { core }); await waitForText(screen.lastFrame, memoryId); await screen.press("return"); @@ -72,7 +72,7 @@ describe("Memory record list flow", () => { }); test("reveals the scope input only after enter and hides it again on escape", async () => { - const screen = renderScreen("/agentcore/memory/record/list/memory-1"); + const screen = renderImperativeScreen("/agentcore/memory/record/list/memory-1"); const fieldHelp = "the namespace value used to scope this request"; await waitForText(screen.lastFrame, "scope type"); @@ -95,7 +95,7 @@ describe("Memory record list flow", () => { core.memory.setListMemoryRecordsResponse({ memoryRecordSummaries: [recordSummary()], }); - const screen = renderScreen("/agentcore/memory/record/list", { core }); + const screen = renderImperativeScreen("/agentcore/memory/record/list", { core }); await waitForText(screen.lastFrame, memoryId); await screen.press("return"); @@ -116,7 +116,7 @@ describe("Memory record list flow", () => { core.memory.setListMemoryRecordsResponse({ memoryRecordSummaries: [recordSummary()], }); - const screen = renderScreen("/agentcore/memory/record/list/memory-1", { core }); + const screen = renderImperativeScreen("/agentcore/memory/record/list/memory-1", { core }); await waitForText(screen.lastFrame, "scope type"); await screen.press("down"); @@ -139,7 +139,7 @@ describe("Memory record list flow", () => { core.memory.setListMemoryRecordsResponse({ memoryRecordSummaries: [recordSummary()], }); - const screen = renderScreen("/agentcore/memory/record/list/memory-1", { + const screen = renderImperativeScreen("/agentcore/memory/record/list/memory-1", { core, endpointUrl: memoryEndpointUrl, }); @@ -181,7 +181,7 @@ describe("Memory record list flow", () => { }), ], }); - const screen = renderScreen( + const screen = renderImperativeScreen( "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers%2Facme", { core }, ); @@ -195,7 +195,7 @@ describe("Memory record list flow", () => { core.memory.setListMemoryRecordsResponse({ memoryRecordSummaries: [recordSummary()], }); - const screen = renderScreen("/agentcore/memory/record/list/memory-1", { core }); + const screen = renderImperativeScreen("/agentcore/memory/record/list/memory-1", { core }); await waitForText(screen.lastFrame, "scope type"); await screen.press("down"); @@ -233,7 +233,7 @@ describe("Memory record list flow", () => { ], }); core.memory.setGetMemoryRecordResponse(response); - const screen = renderScreen( + const screen = renderImperativeScreen( "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers%2Facme", { core }, ); @@ -275,7 +275,7 @@ describe("Memory record list flow", () => { }), ], }); - const screen = renderScreen( + const screen = renderImperativeScreen( "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers%2Facme", { core }, ); @@ -301,9 +301,12 @@ describe("Memory record list flow", () => { nextToken: "page-2", }); core.memory.setListMemoryRecordsResponse({ memoryRecordSummaries: [] }, "page-2"); - const screen = renderScreen("/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers", { - core, - }); + const screen = renderImperativeScreen( + "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers", + { + core, + }, + ); await waitForText(screen.lastFrame, "page 1 · more →"); await screen.write("l"); @@ -311,15 +314,20 @@ describe("Memory record list flow", () => { }); test("shows the scoped empty state and retries list failures", async () => { - const empty = renderScreen("/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers"); + const empty = renderImperativeScreen( + "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers", + ); await waitForText(empty.lastFrame, "No Memory records found for namespace /customers."); empty.unmount(); const core = new TestCoreClient(); core.memory.setError(new Error("records unavailable")); - const failed = renderScreen("/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers", { - core, - }); + const failed = renderImperativeScreen( + "/agentcore/memory/record/list/memory-1/namespace/%2Fcustomers", + { + core, + }, + ); await waitForText(failed.lastFrame, "records unavailable"); expect(failed.lastFrame()).toContain("[r] retry"); @@ -333,7 +341,7 @@ describe("Memory record list flow", () => { }); test("requires a non-empty namespace value", async () => { - const screen = renderScreen("/agentcore/memory/record/list/memory-1"); + const screen = renderImperativeScreen("/agentcore/memory/record/list/memory-1"); await waitForText(screen.lastFrame, "scope type"); await screen.press("return"); // focus the namespace input diff --git a/src/handlers/memory/session/session.screen.test.tsx b/src/handlers/memory/session/session.screen.test.tsx index 3cf3c334ac..3c995d2695 100644 --- a/src/handlers/memory/session/session.screen.test.tsx +++ b/src/handlers/memory/session/session.screen.test.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { Event, SessionSummary } from "@aws-sdk/client-bedrock-agentcore"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitForText, } from "../../../testing"; @@ -42,7 +42,7 @@ describe("Memory session list flow", () => { core.memory.setListEventsResponse({ events: [event({ memoryId, actorId, sessionId })], }); - const screen = renderScreen(`/agentcore/memory/session/list/${memoryId}/${actorId}`, { + const screen = renderImperativeScreen(`/agentcore/memory/session/list/${memoryId}/${actorId}`, { core, }); @@ -59,13 +59,13 @@ describe("Memory session list flow", () => { }); test("shows empty and retry states for session lists", async () => { - const empty = renderScreen("/agentcore/memory/session/list/memory-1/actor-1"); + const empty = renderImperativeScreen("/agentcore/memory/session/list/memory-1/actor-1"); await waitForText(empty.lastFrame, "No sessions found for actor actor-1."); empty.unmount(); const core = new TestCoreClient(); core.memory.setError(new Error("sessions unavailable")); - const failed = renderScreen("/agentcore/memory/session/list/memory-1/actor-1", { + const failed = renderImperativeScreen("/agentcore/memory/session/list/memory-1/actor-1", { core, }); diff --git a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx index 2aee43f723..b7ff3e9421 100644 --- a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx +++ b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx @@ -7,7 +7,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -106,7 +106,7 @@ describe("Runtime endpoint flow", () => { core.runtime.setListEndpointsResponse({ runtimeEndpoints: [endpoint()], }); - const r = renderScreen("/agentcore/runtime/endpoint/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list", { core }); await waitForText(r.lastFrame, runtimeId); await r.press("return"); @@ -124,7 +124,7 @@ describe("Runtime endpoint flow", () => { core.runtime.setListEndpointsResponse({ runtimeEndpoints: [endpoint()], }); - renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { + renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core, endpointUrl: runtimeEndpointUrl, }); @@ -160,7 +160,7 @@ describe("Runtime endpoint flow", () => { }), ], }); - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); await waitForText(r.lastFrame, "production"); const frame = r.lastFrame()!; @@ -177,7 +177,7 @@ describe("Runtime endpoint flow", () => { }); test("shows the Runtime-scoped empty state", async () => { - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123"); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123"); await waitForText(r.lastFrame, "This Runtime has no endpoints."); expect(r.lastFrame()).toContain("runtime-123"); @@ -190,7 +190,7 @@ describe("Runtime endpoint flow", () => { nextToken: "page-2", }); core.runtime.setListEndpointsResponse({ runtimeEndpoints: [] }, "page-2"); - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -201,7 +201,7 @@ describe("Runtime endpoint flow", () => { test("names the selected Runtime in the error state", async () => { const core = new TestCoreClient(); core.runtime.setError(new Error("endpoint access denied")); - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); await waitForText(r.lastFrame, "Error loading endpoints for Runtime runtime-123"); expect(r.lastFrame()).toContain("endpoint access denied"); @@ -214,7 +214,7 @@ describe("Runtime endpoint flow", () => { runtimeEndpoints: [endpoint({ name: qualifier, id: qualifier })], }); core.runtime.setGetEndpointResponse(getEndpointResponse({ name: qualifier, id: qualifier })); - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core, endpointUrl: runtimeEndpointUrl, }); @@ -246,7 +246,7 @@ describe("Runtime endpoint flow", () => { test("shows the endpoint failure reason only when the service provides one", async () => { const healthyCore = new TestCoreClient(); healthyCore.runtime.setGetEndpointResponse(getEndpointResponse()); - const healthy = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { + const healthy = renderImperativeScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { core: healthyCore, }); @@ -261,7 +261,7 @@ describe("Runtime endpoint flow", () => { failureReason: "Endpoint failed its health check", }), ); - const failed = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { + const failed = renderImperativeScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { core: failedCore, }); @@ -272,7 +272,7 @@ describe("Runtime endpoint flow", () => { test("opens complete endpoint JSON from the detail action and returns to the summary", async () => { const core = new TestCoreClient(); core.runtime.setGetEndpointResponse(getEndpointResponse()); - const r = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { core }); await waitForText(r.lastFrame, "invoke this Runtime endpoint"); await r.press("down"); @@ -300,7 +300,7 @@ describe("Runtime endpoint flow", () => { .setListEndpointsResponse({ runtimeEndpoints: [endpoint()] }) .setGetEndpointResponse(getEndpointResponse()) .setGetResponse(getRuntimeResponse()); - const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/list/runtime-123", { core }); await waitForText(r.lastFrame, "prod"); await r.press("return"); @@ -321,7 +321,7 @@ describe("Runtime endpoint flow", () => { parentCore.runtime.setListResponse({ agentRuntimes: [runtime()], }); - const parent = renderScreen("/agentcore/runtime/endpoint/list", { + const parent = renderImperativeScreen("/agentcore/runtime/endpoint/list", { core: parentCore, }); await waitForText(parent.lastFrame, "runtime-123"); @@ -340,7 +340,7 @@ describe("Runtime endpoint flow", () => { runtimeEndpoints: [endpoint()], }); listCore.runtime.setGetEndpointResponse(getEndpointResponse()); - const list = renderScreen("/agentcore/runtime/endpoint/list", { core: listCore }); + const list = renderImperativeScreen("/agentcore/runtime/endpoint/list", { core: listCore }); await waitForText(list.lastFrame, "runtime-123"); await list.press("return"); await waitForText(list.lastFrame, "prod"); @@ -360,7 +360,7 @@ describe("Runtime endpoint flow", () => { core.runtime.setListResponse({ agentRuntimes: [runtime({ agentRuntimeId: "redirect-parent" })], }); - const r = renderScreen("/agentcore/runtime/endpoint/get", { core }); + const r = renderImperativeScreen("/agentcore/runtime/endpoint/get", { core }); await waitForText(r.lastFrame, "redirect-parent"); expect(core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(true); diff --git a/src/handlers/runtime/invoke/invoke.screen.test.tsx b/src/handlers/runtime/invoke/invoke.screen.test.tsx index f6fba007fe..840d02e02e 100644 --- a/src/handlers/runtime/invoke/invoke.screen.test.tsx +++ b/src/handlers/runtime/invoke/invoke.screen.test.tsx @@ -8,7 +8,7 @@ import type { import type { RuntimeInvokeRequest } from "../types"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -90,7 +90,7 @@ describe("Runtime invoke routing", () => { .setGetResponse({ agentRuntimeArn: `arn:aws:bedrock-agentcore:${REGION}:123456789012:runtime/${runtimeId}`, } as GetAgentRuntimeResponse); - const screen = renderScreen("/agentcore/runtime/invoke", { core }); + const screen = renderImperativeScreen("/agentcore/runtime/invoke", { core }); await waitForText(screen.lastFrame, runtimeId); await screen.press("return"); @@ -109,7 +109,7 @@ describe("Runtime invoke routing", () => { core.runtime.setListEndpointsResponse({ runtimeEndpoints: [endpoint()] }).setListResponse({ agentRuntimes: [runtime({ agentRuntimeId: "back-to-runtime-picker" })], }); - const screen = renderScreen(`/agentcore/runtime/invoke/${RUNTIME_ID}`, { core }); + const screen = renderImperativeScreen(`/agentcore/runtime/invoke/${RUNTIME_ID}`, { core }); await waitForText(screen.lastFrame, QUALIFIER); await screen.press("escape"); @@ -124,7 +124,7 @@ describe("Runtime invoke routing", () => { core.runtime .setListEndpointsResponse({ runtimeEndpoints: [endpoint()] }) .setGetResponse({ agentRuntimeArn: RUNTIME_ARN } as GetAgentRuntimeResponse); - const screen = renderScreen(`/agentcore/runtime/invoke/${RUNTIME_ID}`, { + const screen = renderImperativeScreen(`/agentcore/runtime/invoke/${RUNTIME_ID}`, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -153,7 +153,7 @@ describe("Runtime invoke routing", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -187,7 +187,7 @@ describe("Runtime invoke routing", () => { name: "AccessDeniedException", }); }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "AccessDeniedException"); await waitForText(screen.lastFrame, "not authorized for this Runtime"); @@ -200,7 +200,7 @@ describe("Runtime invoke routing", () => { lookupSignal = signal; return new Promise(() => {}); }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitFor(() => lookupSignal !== undefined); screen.unmount(); @@ -219,7 +219,7 @@ describe("Runtime invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Enter JSON payload"); expect(screen.lastFrame()!.split("\n")).not.toContain("JSON payload"); @@ -242,7 +242,7 @@ describe("Runtime invoke JSON console", () => { test("rejects invalid JSON locally without clearing the editor or invoking", async () => { const core = new TestCoreClient(); core.runtime.setGetResponse({ agentRuntimeArn: RUNTIME_ARN } as GetAgentRuntimeResponse); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write('{"prompt":'); @@ -262,7 +262,7 @@ describe("Runtime invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{"); @@ -289,7 +289,7 @@ describe("Runtime invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write('{"a":1}'); @@ -310,7 +310,7 @@ describe("Runtime invoke JSON console", () => { test("keeps blank multiline rows inside the four-line editor", async () => { const core = new TestCoreClient(); core.runtime.setGetResponse({ agentRuntimeArn: RUNTIME_ARN } as GetAgentRuntimeResponse); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); const initialStatusLine = screen @@ -338,7 +338,7 @@ describe("Runtime invoke JSON console", () => { core.runtime.setGetResponse({ agentRuntimeArn: `arn:aws:bedrock-agentcore:${REGION}:123456789012:runtime/${runtimeId}`, } as GetAgentRuntimeResponse); - const screen = renderScreen(`/agentcore/runtime/invoke/${runtimeId}/${QUALIFIER}`, { + const screen = renderImperativeScreen(`/agentcore/runtime/invoke/${runtimeId}/${QUALIFIER}`, { core, }); @@ -373,7 +373,7 @@ describe("Runtime invoke JSON console", () => { : responseBody(Buffer.from("done")), }; }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write('{"turn":1}'); @@ -405,7 +405,7 @@ describe("Runtime invoke JSON console", () => { yield Buffer.from(" response"); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -437,7 +437,7 @@ describe("Runtime invoke JSON console", () => { runtimeSessionId: "returned-runtime", body: responseBody(Buffer.from("data: one\n\ndata: two\n\n")), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -472,7 +472,7 @@ describe("Runtime invoke JSON console", () => { contentType, body: responseBody(body), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -488,7 +488,7 @@ describe("Runtime invoke JSON console", () => { core.runtime.invokeRuntime = async () => { throw Object.assign(new Error("connection failed"), { name: "NetworkError" }); }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -510,7 +510,7 @@ describe("Runtime invoke JSON console", () => { core.runtime.invokeRuntime = async () => { throw error; }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -530,7 +530,7 @@ describe("Runtime invoke JSON console", () => { core.runtime.invokeRuntime = async () => { throw failure; }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -552,7 +552,7 @@ describe("Runtime invoke JSON console", () => { throw Object.assign(new Error("stream failed"), { name: "StreamReadError" }); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -574,7 +574,7 @@ describe("Runtime invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from(response)), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await screen.resize(80, 16); await waitForText(screen.lastFrame, "Ready"); @@ -607,7 +607,7 @@ describe("Runtime invoke JSON console", () => { }; }; const initialSession = "cli-selected-session"; - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -653,7 +653,7 @@ describe("Runtime invoke JSON console", () => { body: responseBody(Buffer.from('{"ok":true}')), }; }; - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -706,7 +706,7 @@ describe("Runtime invoke JSON console", () => { mcpProtocolVersion: "2025-06-18", body: responseBody(Buffer.from("old response")), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -782,7 +782,7 @@ describe("Runtime invoke JSON console", () => { contentType: "text/plain", body: responseBody(Buffer.from("ok")), }); - const screen = renderScreen(CONSOLE_PATH, { + const screen = renderImperativeScreen(CONSOLE_PATH, { core, withContext: (ctx) => ctx.withValue(RuntimeInvokeLaunchContextKey, { @@ -826,7 +826,7 @@ describe("Runtime invoke JSON console", () => { contentType: "application/json", body: responseBody(Buffer.from(raw)), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -853,7 +853,7 @@ describe("Runtime invoke JSON console", () => { yield Buffer.from([0, 255]); })(), }); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -872,7 +872,7 @@ describe("Runtime invoke JSON console", () => { agentRuntimeArn: RUNTIME_ARN, authorizerConfiguration: { customJWTAuthorizer: {} }, } as GetAgentRuntimeResponse); - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); await waitForText(screen.lastFrame, "Ready"); await screen.write("{}"); @@ -900,7 +900,7 @@ describe("Runtime invoke JSON console", () => { })(), }; }; - const screen = renderScreen(CONSOLE_PATH, { core }); + const screen = renderImperativeScreen(CONSOLE_PATH, { core }); try { await waitForText(screen.lastFrame, "Ready"); diff --git a/src/handlers/runtime/runtime.screen.test.tsx b/src/handlers/runtime/runtime.screen.test.tsx index 27db8213be..44d538881d 100644 --- a/src/handlers/runtime/runtime.screen.test.tsx +++ b/src/handlers/runtime/runtime.screen.test.tsx @@ -6,7 +6,7 @@ import type { import { QueryClient } from "@tanstack/react-query"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, tick, waitFor, @@ -80,7 +80,7 @@ describe("runtime picker", () => { lastUpdatedAt: new Date("2026-07-19T01:02:03.000Z"), }), ]); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "orders"); const frame = r.lastFrame()!; @@ -98,7 +98,7 @@ describe("runtime picker", () => { test("calls listRuntimes once with exact Core options", async () => { const core = coreWithRuntimes([runtime()]); - renderScreen("/agentcore/runtime/list", { core, endpointUrl: runtimeEndpointUrl }); + renderImperativeScreen("/agentcore/runtime/list", { core, endpointUrl: runtimeEndpointUrl }); await waitFor(() => core.runtime.calls.some((call) => call.method === "listRuntimes")); expect(core.runtime.calls.filter((call) => call.method === "listRuntimes")).toEqual([ @@ -117,7 +117,7 @@ describe("runtime picker", () => { }); test("shows the first-page empty state", async () => { - const r = renderScreen("/agentcore/runtime/list"); + const r = renderImperativeScreen("/agentcore/runtime/list"); await waitForText(r.lastFrame, "No Runtimes found in this Region."); }); @@ -129,7 +129,7 @@ describe("runtime picker", () => { nextToken: "page-2", }); core.runtime.setListResponse({ agentRuntimes: [] }, "page-2"); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -139,7 +139,7 @@ describe("runtime picker", () => { test("Esc returns to the Runtime menu from a successful direct entry", async () => { const core = coreWithRuntimes([runtime()]); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "checkout"); await r.press("escape"); @@ -151,7 +151,7 @@ describe("runtime picker", () => { const core = coreWithRuntimes([ runtime({ agentRuntimeId: "redirected-Ab12Cd34Ef", agentRuntimeName: "redirected" }), ]); - const r = renderScreen("/agentcore/runtime/get", { core }); + const r = renderImperativeScreen("/agentcore/runtime/get", { core }); await waitForText(r.lastFrame, "redirected"); expect(core.runtime.calls[0]?.method).toBe("listRuntimes"); @@ -162,7 +162,7 @@ describe("runtime hub", () => { test("fetches the route ID with exact Core options and renders its summary", async () => { const core = new TestCoreClient(); core.runtime.setGetResponse(getRuntimeResponse()); - const r = renderScreen("/agentcore/runtime/get/runtime-123", { + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core, endpointUrl: runtimeEndpointUrl, }); @@ -190,7 +190,7 @@ describe("runtime hub", () => { test("shows the Runtime failure reason only when the service provides one", async () => { const healthyCore = new TestCoreClient(); healthyCore.runtime.setGetResponse(getRuntimeResponse()); - const healthy = renderScreen("/agentcore/runtime/get/runtime-123", { + const healthy = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core: healthyCore, }); @@ -205,7 +205,7 @@ describe("runtime hub", () => { failureReason: "Image could not be pulled", }), ); - const failed = renderScreen("/agentcore/runtime/get/runtime-123", { + const failed = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core: failedCore, }); @@ -216,7 +216,7 @@ describe("runtime hub", () => { test("renders invoke first with endpoint, version, and detail actions", async () => { const core = new TestCoreClient(); core.runtime.setGetResponse(getRuntimeResponse()); - const r = renderScreen("/agentcore/runtime/get/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core }); await waitForText(r.lastFrame, "show the full JSON definition"); const frame = r.lastFrame()!; @@ -240,7 +240,7 @@ describe("runtime hub", () => { agentRuntimeName: "encoded-runtime", }), ); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, runtimeId); await r.press("return"); @@ -280,7 +280,7 @@ describe("runtime hub", () => { ], }); } - const r = renderScreen(`/agentcore/runtime/get/${encodeURIComponent(runtimeId)}`, { + const r = renderImperativeScreen(`/agentcore/runtime/get/${encodeURIComponent(runtimeId)}`, { core, }); @@ -310,7 +310,7 @@ describe("runtime hub", () => { ), }), ); - const r = renderScreen("/agentcore/runtime/get/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core }); await waitForText(r.lastFrame, "show the full JSON definition"); for (let index = 0; index < 4; index += 1) await r.press("down"); @@ -333,7 +333,7 @@ describe("runtime hub", () => { test("retries a failed hub query without leaving the route", async () => { const core = new TestCoreClient(); core.runtime.setError(new Error("runtime unavailable")); - const r = renderScreen("/agentcore/runtime/get/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core }); await waitForText(r.lastFrame, "runtime unavailable"); expect(r.lastFrame()).toContain("agentcore → runtime → get → runtime-123"); @@ -355,7 +355,7 @@ describe("runtime hub", () => { queries: { retry: false, gcTime: Infinity, staleTime: 0 }, }, }); - const r = renderScreen("/agentcore/runtime/get/runtime-123", { + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123", { core, queryClient, }); @@ -377,7 +377,7 @@ describe("runtime hub", () => { test("retries a failed JSON query without leaving the route", async () => { const core = new TestCoreClient(); core.runtime.setError(new Error("detail unavailable")); - const r = renderScreen("/agentcore/runtime/get/runtime-123/json", { core }); + const r = renderImperativeScreen("/agentcore/runtime/get/runtime-123/json", { core }); await waitForText(r.lastFrame, "detail unavailable"); expect(r.lastFrame()).toContain("[r] retry"); @@ -393,7 +393,7 @@ describe("runtime hub", () => { test("Esc from the hub returns through history to the Runtime picker", async () => { const core = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]); core.runtime.setGetResponse(getRuntimeResponse()); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "runtime-123"); await r.press("return"); @@ -405,7 +405,7 @@ describe("runtime hub", () => { test("Esc from Runtime JSON returns through history to the Runtime hub", async () => { const core = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]); core.runtime.setGetResponse(getRuntimeResponse()); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "runtime-123"); await r.press("return"); @@ -438,7 +438,7 @@ describe("runtime hub", () => { }, ], }); - const r = renderScreen("/agentcore/runtime/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/list", { core }); await waitForText(r.lastFrame, "runtime-123"); await r.press("return"); @@ -464,7 +464,7 @@ describe("runtime hub", () => { const hubCore = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]); const hubPending = Promise.withResolvers(); hubCore.runtime.getRuntime = async () => hubPending.promise; - const hub = renderScreen("/agentcore/runtime/list", { core: hubCore }); + const hub = renderImperativeScreen("/agentcore/runtime/list", { core: hubCore }); await waitForText(hub.lastFrame, "runtime-123"); await hub.press("return"); @@ -478,7 +478,7 @@ describe("runtime hub", () => { hubCore.runtime.getRuntime = async () => { throw new Error("hub failed"); }; - const hub = renderScreen("/agentcore/runtime/list", { core: hubCore }); + const hub = renderImperativeScreen("/agentcore/runtime/list", { core: hubCore }); await waitForText(hub.lastFrame, "runtime-123"); await hub.press("return"); @@ -489,7 +489,7 @@ describe("runtime hub", () => { const jsonCore = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]); jsonCore.runtime.setGetResponse(getRuntimeResponse()); - const json = renderScreen("/agentcore/runtime/list", { core: jsonCore }); + const json = renderImperativeScreen("/agentcore/runtime/list", { core: jsonCore }); await waitForText(json.lastFrame, "runtime-123"); await json.press("return"); diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index be2432720f..fce8071780 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -5,7 +5,7 @@ import { ValueContext } from "../../../router"; import type { RuntimeShellSession } from "../types"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, tick, type TtyInput, @@ -80,7 +80,7 @@ async function interruptUntilExit(rendering: Promise, stdin: TtyInput): Pr describe("RuntimeShellScreen", () => { test("a direct Runtime route skips the Runtime picker", async () => { - const screen = renderScreen("/agentcore/runtime/shell/checkout-AbCdEf1234", { + const screen = renderImperativeScreen("/agentcore/runtime/shell/checkout-AbCdEf1234", { core: core(), }); diff --git a/src/handlers/runtime/version/version.screen.test.tsx b/src/handlers/runtime/version/version.screen.test.tsx index bb350b00d5..c189f1686a 100644 --- a/src/handlers/runtime/version/version.screen.test.tsx +++ b/src/handlers/runtime/version/version.screen.test.tsx @@ -5,7 +5,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, - renderImperativeScreen as renderScreen, + renderImperativeScreen, TestCoreClient, waitFor, waitForText, @@ -72,7 +72,7 @@ describe("Runtime version flow", () => { core.runtime.setListVersionsResponse({ agentRuntimes: [runtime({ agentRuntimeId: runtimeId, agentRuntimeVersion: "9" })], }); - const r = renderScreen("/agentcore/runtime/version/list", { core }); + const r = renderImperativeScreen("/agentcore/runtime/version/list", { core }); await waitForText(r.lastFrame, runtimeId); await r.press("return"); @@ -90,7 +90,7 @@ describe("Runtime version flow", () => { core.runtime.setListVersionsResponse({ agentRuntimes: [runtime()], }); - renderScreen("/agentcore/runtime/version/list/runtime-123", { + renderImperativeScreen("/agentcore/runtime/version/list/runtime-123", { core, endpointUrl: runtimeEndpointUrl, }); @@ -133,7 +133,7 @@ describe("Runtime version flow", () => { }), ], }); - const r = renderScreen("/agentcore/runtime/version/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/version/list/runtime-123", { core }); await waitForText(r.lastFrame, "UPDATE_FAILED"); const frame = r.lastFrame()!; @@ -162,7 +162,7 @@ describe("Runtime version flow", () => { nextToken: "page-2", }); core.runtime.setListVersionsResponse({ agentRuntimes: [] }, "page-2"); - const r = renderScreen("/agentcore/runtime/version/list/runtime-123", { core }); + const r = renderImperativeScreen("/agentcore/runtime/version/list/runtime-123", { core }); await waitForText(r.lastFrame, "page 1 · more →"); await r.write("l"); @@ -171,13 +171,13 @@ describe("Runtime version flow", () => { }); test("names the selected Runtime in empty and error states", async () => { - const empty = renderScreen("/agentcore/runtime/version/list/runtime-123"); + const empty = renderImperativeScreen("/agentcore/runtime/version/list/runtime-123"); await waitForText(empty.lastFrame, "No versions found for Runtime runtime-123."); empty.unmount(); const core = new TestCoreClient(); core.runtime.setError(new Error("version access denied")); - const error = renderScreen("/agentcore/runtime/version/list/runtime-123", { core }); + const error = renderImperativeScreen("/agentcore/runtime/version/list/runtime-123", { core }); await waitForText(error.lastFrame, "Error loading versions for Runtime runtime-123"); expect(error.lastFrame()).toContain("version access denied"); expect(error.lastFrame()).toContain("[r] retry"); @@ -189,7 +189,7 @@ describe("Runtime version flow", () => { agentRuntimes: [runtime({ agentRuntimeVersion: "9" })], }); core.runtime.setGetVersionResponse(getVersionResponse({ agentRuntimeVersion: "9" })); - const r = renderScreen("/agentcore/runtime/version/list/runtime-123", { + const r = renderImperativeScreen("/agentcore/runtime/version/list/runtime-123", { core, endpointUrl: runtimeEndpointUrl, }); @@ -217,7 +217,7 @@ describe("Runtime version flow", () => { parentCore.runtime.setListResponse({ agentRuntimes: [runtime()], }); - const parent = renderScreen("/agentcore/runtime/version/list", { + const parent = renderImperativeScreen("/agentcore/runtime/version/list", { core: parentCore, }); await waitForText(parent.lastFrame, "runtime-123"); @@ -236,7 +236,7 @@ describe("Runtime version flow", () => { agentRuntimes: [runtime()], }); listCore.runtime.setGetVersionResponse(getVersionResponse()); - const list = renderScreen("/agentcore/runtime/version/list", { core: listCore }); + const list = renderImperativeScreen("/agentcore/runtime/version/list", { core: listCore }); await waitForText(list.lastFrame, "runtime-123"); await list.press("return"); await waitForText(list.lastFrame, "agentcore → runtime → version → list → runtime-123"); @@ -260,7 +260,7 @@ describe("Runtime version flow", () => { core.runtime.setListResponse({ agentRuntimes: [runtime({ agentRuntimeId: "redirect-parent" })], }); - const r = renderScreen("/agentcore/runtime/version/get", { core }); + const r = renderImperativeScreen("/agentcore/runtime/version/get", { core }); await waitForText(r.lastFrame, "redirect-parent"); expect(core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(true);