diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3283799629..54c108477d 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 add` — 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 6cf51764f8..3845176a7a 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 status --json # scriptable, machine-readable output ``` ## What problem does it solve? @@ -44,34 +44,28 @@ agentcore 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 | -| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| `create`, `add`, `export`, `remove`, `dev`, `deploy`, `invoke`, `log`, `traces`, `status`, `build` | 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 | +Project commands manage local project specifications and their deployments. +`eval` commands evaluate deployed resources without requiring a local project. + +| Command | Purpose | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `create`, `add`, `export`, `remove`, `dev`, `deploy`, `invoke`, `log`, `traces`, `status`, `build` | 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 add --help -agentcore runtime invoke --help +agentcore 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 create` for guided setup. To create a default project without the wizard, run `agentcore create --name MyAssistant`. diff --git a/command.md b/command.md index 8ce9f3f124..88b03fdb33 100644 --- a/command.md +++ b/command.md @@ -9,150 +9,49 @@ This reference was generated from `agentcore --help` for version `1.0.0-rc.4`. - [Global options](#global-options) - [`agentcore`](#agentcore) - [Project commands](#project-commands) - - [`agentcore project`](#agentcore-project) - - [`agentcore project create`](#agentcore-project-create) - - [`agentcore project add`](#agentcore-project-add) - - [`agentcore project add config-bundle`](#agentcore-project-add-config-bundle) - - [`agentcore project add harness`](#agentcore-project-add-harness) - - [`agentcore project add memory`](#agentcore-project-add-memory) - - [`agentcore project add runtime`](#agentcore-project-add-runtime) - - [`agentcore project add online-eval`](#agentcore-project-add-online-eval) - - [`agentcore project add online-insight`](#agentcore-project-add-online-insight) - - [`agentcore project add evaluator`](#agentcore-project-add-evaluator) - - [`agentcore project add evaluator llm-as-a-judge`](#agentcore-project-add-evaluator-llm-as-a-judge) - - [`agentcore project add evaluator code-based`](#agentcore-project-add-evaluator-code-based) - - [`agentcore project add credentials`](#agentcore-project-add-credentials) - - [`agentcore project add credentials api-key`](#agentcore-project-add-credentials-api-key) - - [`agentcore project add credentials oauth`](#agentcore-project-add-credentials-oauth) - - [`agentcore project add credentials payment`](#agentcore-project-add-credentials-payment) - - [`agentcore project add gateway`](#agentcore-project-add-gateway) - - [`agentcore project add gateway-target`](#agentcore-project-add-gateway-target) - - [`agentcore project add gateway-connector`](#agentcore-project-add-gateway-connector) - - [`agentcore project add policy-engine`](#agentcore-project-add-policy-engine) - - [`agentcore project add policy`](#agentcore-project-add-policy) - - [`agentcore project add payment-manager`](#agentcore-project-add-payment-manager) - - [`agentcore project add payment-connector`](#agentcore-project-add-payment-connector) - - [`agentcore project add runtime-endpoint`](#agentcore-project-add-runtime-endpoint) - - [`agentcore project export`](#agentcore-project-export) - - [`agentcore project export harness`](#agentcore-project-export-harness) - - [`agentcore project remove`](#agentcore-project-remove) - - [`agentcore project dev`](#agentcore-project-dev) - - [`agentcore project deploy`](#agentcore-project-deploy) - - [`agentcore project invoke`](#agentcore-project-invoke) - - [`agentcore project invoke runtime`](#agentcore-project-invoke-runtime) - - [`agentcore project invoke harness`](#agentcore-project-invoke-harness) - - [`agentcore project log`](#agentcore-project-log) - - [`agentcore project log runtime`](#agentcore-project-log-runtime) - - [`agentcore project log harness`](#agentcore-project-log-harness) - - [`agentcore project traces`](#agentcore-project-traces) - - [`agentcore project traces runtime`](#agentcore-project-traces-runtime) - - [`agentcore project traces runtime list`](#agentcore-project-traces-runtime-list) - - [`agentcore project traces runtime get`](#agentcore-project-traces-runtime-get) - - [`agentcore project traces harness`](#agentcore-project-traces-harness) - - [`agentcore project traces harness list`](#agentcore-project-traces-harness-list) - - [`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) + - [`agentcore create`](#agentcore-create) + - [`agentcore add`](#agentcore-add) + - [`agentcore add config-bundle`](#agentcore-add-config-bundle) + - [`agentcore add harness`](#agentcore-add-harness) + - [`agentcore add memory`](#agentcore-add-memory) + - [`agentcore add runtime`](#agentcore-add-runtime) + - [`agentcore add online-eval`](#agentcore-add-online-eval) + - [`agentcore add online-insight`](#agentcore-add-online-insight) + - [`agentcore add evaluator`](#agentcore-add-evaluator) + - [`agentcore add evaluator llm-as-a-judge`](#agentcore-add-evaluator-llm-as-a-judge) + - [`agentcore add evaluator code-based`](#agentcore-add-evaluator-code-based) + - [`agentcore add credentials`](#agentcore-add-credentials) + - [`agentcore add credentials api-key`](#agentcore-add-credentials-api-key) + - [`agentcore add credentials oauth`](#agentcore-add-credentials-oauth) + - [`agentcore add credentials payment`](#agentcore-add-credentials-payment) + - [`agentcore add gateway`](#agentcore-add-gateway) + - [`agentcore add gateway-target`](#agentcore-add-gateway-target) + - [`agentcore add gateway-connector`](#agentcore-add-gateway-connector) + - [`agentcore add policy-engine`](#agentcore-add-policy-engine) + - [`agentcore add policy`](#agentcore-add-policy) + - [`agentcore add payment-manager`](#agentcore-add-payment-manager) + - [`agentcore add payment-connector`](#agentcore-add-payment-connector) + - [`agentcore add runtime-endpoint`](#agentcore-add-runtime-endpoint) + - [`agentcore export`](#agentcore-export) + - [`agentcore export harness`](#agentcore-export-harness) + - [`agentcore remove`](#agentcore-remove) + - [`agentcore dev`](#agentcore-dev) + - [`agentcore deploy`](#agentcore-deploy) + - [`agentcore invoke`](#agentcore-invoke) + - [`agentcore invoke runtime`](#agentcore-invoke-runtime) + - [`agentcore invoke harness`](#agentcore-invoke-harness) + - [`agentcore log`](#agentcore-log) + - [`agentcore log runtime`](#agentcore-log-runtime) + - [`agentcore log harness`](#agentcore-log-harness) + - [`agentcore traces`](#agentcore-traces) + - [`agentcore traces runtime`](#agentcore-traces-runtime) + - [`agentcore traces runtime list`](#agentcore-traces-runtime-list) + - [`agentcore traces runtime get`](#agentcore-traces-runtime-get) + - [`agentcore traces harness`](#agentcore-traces-harness) + - [`agentcore traces harness list`](#agentcore-traces-harness-list) + - [`agentcore traces harness get`](#agentcore-traces-harness-get) + - [`agentcore status`](#agentcore-status) + - [`agentcore build`](#agentcore-build) - [Evaluation commands](#evaluation-commands) - [`agentcore eval`](#agentcore-eval) - [`agentcore eval evaluator`](#agentcore-eval-evaluator) @@ -250,18 +149,10 @@ the platform for production AI agents ## Project commands -### `agentcore project` +### `agentcore create` ```text -agentcore project [options] [command] -``` - -manage an AgentCore project - -#### `agentcore project create` - -```text -agentcore project create [options] +agentcore create [options] ``` create a new AgentCore project @@ -275,18 +166,18 @@ create a new AgentCore project - `--skip-install`: skip installing dependencies (npm install, uv sync) (default: false) - `--skip-git`: skip initializing a git repository (default: false) -#### `agentcore project add` +### `agentcore add` ```text -agentcore project add [options] [command] +agentcore add [options] [command] ``` add project resources -##### `agentcore project add config-bundle` +#### `agentcore add config-bundle` ```text -agentcore project add config-bundle [options] +agentcore add config-bundle [options] ``` add a configuration bundle to the current project @@ -300,10 +191,10 @@ add a configuration bundle to the current project - `--commit-message `: message describing the initial configuration - `--kms-key-arn `: customer managed KMS key ARN for component configurations -##### `agentcore project add harness` +#### `agentcore add harness` ```text -agentcore project add harness [options] +agentcore add harness [options] ``` add a harness to the current project @@ -335,10 +226,10 @@ add a harness to the current project - `--tags `: tags as key=value (repeatable) or JSON object - `--dockerfile `: path to local dockerfile to use as the container image for the harness -##### `agentcore project add memory` +#### `agentcore add memory` ```text -agentcore project add memory [options] +agentcore add memory [options] ``` add a Memory to the current project @@ -355,10 +246,10 @@ add a Memory to the current project - `--execution-role-arn `: IAM role the Memory assumes; a default role is created when omitted - `--tags `: tags to apply (JSON object of key/value strings) -##### `agentcore project add runtime` +#### `agentcore add runtime` ```text -agentcore project add runtime [options] +agentcore add runtime [options] ``` add a Runtime to the current project @@ -386,10 +277,10 @@ add a Runtime to the current project - `--filesystem-configurations `: filesystem mount configurations (JSON) - `--tags `: tags as key=value (repeatable) or JSON object -##### `agentcore project add online-eval` +#### `agentcore add online-eval` ```text -agentcore project add online-eval [options] +agentcore add online-eval [options] ``` add an online evaluation config to the current project @@ -407,10 +298,10 @@ add an online evaluation config to the current project - `--enable-on-create `: enable evaluation immediately after deploy (default true; pass false to add it paused) - `--tags `: tags to apply (JSON object of key/value strings) -##### `agentcore project add online-insight` +#### `agentcore add online-insight` ```text -agentcore project add online-insight [options] +agentcore add online-insight [options] ``` add an online insight config to the current project @@ -429,18 +320,18 @@ add an online insight config to the current project - `--enable-on-create `: enable insights immediately after deploy (default true; pass false to add it paused) - `--tags `: tags to apply (JSON object of key/value strings) -##### `agentcore project add evaluator` +#### `agentcore add evaluator` ```text -agentcore project add evaluator [options] [command] +agentcore add evaluator [options] [command] ``` add a custom evaluator to the current project -###### `agentcore project add evaluator llm-as-a-judge` +##### `agentcore add evaluator llm-as-a-judge` ```text -agentcore project add evaluator llm-as-a-judge [options] +agentcore add evaluator llm-as-a-judge [options] ``` add an LLM-as-a-Judge evaluator: another LLM prompted with instructions on how to score a session @@ -457,10 +348,10 @@ add an LLM-as-a-Judge evaluator: another LLM prompted with instructions on how t - `--kms-key-arn `: customer-managed KMS key ARN to encrypt the evaluator - `--tags `: tags to apply (JSON object of key/value strings) -###### `agentcore project add evaluator code-based` +##### `agentcore add evaluator code-based` ```text -agentcore project add evaluator code-based [options] +agentcore add evaluator code-based [options] ``` add a code-based evaluator: scaffold a Python Lambda with custom evaluation logic, or reference an existing Lambda with --lambda-arn @@ -475,18 +366,18 @@ add a code-based evaluator: scaffold a Python Lambda with custom evaluation logi - `--kms-key-arn `: customer-managed KMS key ARN to encrypt the evaluator - `--tags `: tags to apply (JSON object of key/value strings) -##### `agentcore project add credentials` +#### `agentcore add credentials` ```text -agentcore project add credentials [options] [command] +agentcore add credentials [options] [command] ``` add AgentCore Identity credential providers to the current project -###### `agentcore project add credentials api-key` +##### `agentcore add credentials api-key` ```text -agentcore project add credentials api-key [options] +agentcore add credentials api-key [options] ``` add an API key credential provider to the current project @@ -497,10 +388,10 @@ add an API key credential provider to the current project - `--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>"} -###### `agentcore project add credentials oauth` +##### `agentcore add credentials oauth` ```text -agentcore project add credentials oauth [options] +agentcore add credentials oauth [options] ``` add an OAuth2 credential provider to the current project @@ -516,10 +407,10 @@ add an OAuth2 credential provider to the current project - `--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>"} -###### `agentcore project add credentials payment` +##### `agentcore add credentials payment` ```text -agentcore project add credentials payment [options] +agentcore add credentials payment [options] ``` add a payment credential provider to the current project @@ -536,10 +427,10 @@ add a payment credential provider to the current project - `--authorization-private-key `: Stripe/Privy authorization private key (file://path or - for stdin; inline values are rejected) - `--authorization-id `: Stripe/Privy authorization identifier -##### `agentcore project add gateway` +#### `agentcore add gateway` ```text -agentcore project add gateway [options] +agentcore add gateway [options] ``` add a Gateway to the current project @@ -558,10 +449,10 @@ add a Gateway to the current project - `--exception-level `: exception detail level: debug - `--tags `: tags as repeated key=value or a JSON object -##### `agentcore project add gateway-target` +#### `agentcore add gateway-target` ```text -agentcore project add gateway-target [options] +agentcore add gateway-target [options] ``` add a Target to a project Gateway @@ -578,10 +469,10 @@ add a Target to a project Gateway - `--credential-name `: name of a compatible credential declared in this project - `--scope `: OAuth scope -##### `agentcore project add gateway-connector` +#### `agentcore add gateway-connector` ```text -agentcore project add gateway-connector [options] +agentcore add gateway-connector [options] ``` add a connector-backed Target to a project Gateway @@ -594,10 +485,10 @@ add a connector-backed Target to a project Gateway - `--connector-configuration `: complete connector agentCoreGateways[].targets[] object (JSON; inline, file://<path>, or - for stdin) - `--knowledge-base `: external ten-character Knowledge Base ID; only for bedrock-knowledge-bases -##### `agentcore project add policy-engine` +#### `agentcore add policy-engine` ```text -agentcore project add policy-engine [options] +agentcore add policy-engine [options] ``` add a Policy Engine to the current project @@ -611,10 +502,10 @@ add a Policy Engine to the current project - `--attach-to-gateways `: names of project Gateways to attach this engine to - `--attach-mode `: attached Gateway enforcement mode: log-only or enforce (default enforce) -##### `agentcore project add policy` +#### `agentcore add policy` ```text -agentcore project add policy [options] +agentcore add policy [options] ``` add a Cedar Policy to a project Policy Engine @@ -629,10 +520,10 @@ add a Cedar Policy to a project Policy Engine - `--enforcement-mode `: enforcement mode: active or log-only - `--authorization-phase `: authorization phase: initiate or return-output (default inferred from the statement) -##### `agentcore project add payment-manager` +#### `agentcore add payment-manager` ```text -agentcore project add payment-manager [options] +agentcore add payment-manager [options] ``` add a payment manager to the current project @@ -651,10 +542,10 @@ add a payment manager to the current project - `--tool-allowlist `: tools eligible for automatic payment - `--network-preferences `: preferred payment networks -##### `agentcore project add payment-connector` +#### `agentcore add payment-connector` ```text -agentcore project add payment-connector [options] +agentcore add payment-connector [options] ``` add a connector to a project payment manager @@ -666,10 +557,10 @@ add a connector to a project payment manager - `--credential `: an existing payment credential to reuse - `--quick-create`: create a CoinbaseCDP connector through Quick Create (default: false) -##### `agentcore project add runtime-endpoint` +#### `agentcore add runtime-endpoint` ```text -agentcore project add runtime-endpoint [options] +agentcore add runtime-endpoint [options] ``` add a named endpoint (version alias) to a runtime @@ -681,18 +572,18 @@ add a named endpoint (version alias) to a runtime - `--version `: the runtime version this endpoint points to (default: 1) - `--description `: description of the endpoint -#### `agentcore project export` +### `agentcore export` ```text -agentcore project export [options] [command] +agentcore export [options] [command] ``` convert project resources into editable code you own -##### `agentcore project export harness` +#### `agentcore export harness` ```text -agentcore project export harness [options] +agentcore export harness [options] ``` convert a harness into an editable Strands Runtime agent @@ -703,10 +594,10 @@ convert a harness into an editable Strands Runtime agent - `--arn `: the ARN of a deployed harness to fetch from the service and export - `--target-agent-name `: the name of the generated Runtime agent (default <harnessName>Agent) -#### `agentcore project remove` +### `agentcore remove` ```text -agentcore project remove [options] [resource] +agentcore remove [options] [resource] ``` remove a resource from the project @@ -724,10 +615,10 @@ remove a resource from the project - `--runtime `: name of the parent runtime for a runtime-endpoint - `--yes`: skip the confirmation prompt when removing all resources (default: false) -#### `agentcore project dev` +### `agentcore dev` ```text -agentcore project dev [options] +agentcore dev [options] ``` run the project locally for development @@ -740,10 +631,10 @@ run the project locally for development - `--mode `: how to run: browser (Agent Inspector web UI) or headless (agents stream to the terminal) (default: "headless") - `--ui-port `: port for the Agent Inspector web UI (browser mode) -#### `agentcore project deploy` +### `agentcore deploy` ```text -agentcore project deploy [options] +agentcore deploy [options] ``` deploy the project to AWS @@ -753,18 +644,18 @@ deploy the project to AWS - `--target `: name of the aws-targets.json entry to deploy; the default target is created automatically from your AWS account and region on first deploy (default: "default") - `--yes`: confirm removing the target's stack when the project declares nothing to deploy (default: false) -#### `agentcore project invoke` +### `agentcore invoke` ```text -agentcore project invoke [options] [command] +agentcore invoke [options] [command] ``` invoke a Runtime or harness from the current project -##### `agentcore project invoke runtime` +#### `agentcore invoke runtime` ```text -agentcore project invoke runtime [options] +agentcore invoke runtime [options] ``` invoke a Runtime from the current project @@ -793,10 +684,10 @@ invoke a Runtime from the current project - `--baggage `: the W3C baggage - `--output-file `: the response output file -##### `agentcore project invoke harness` +#### `agentcore invoke harness` ```text -agentcore project invoke harness [options] +agentcore invoke harness [options] ``` invoke a harness from the current project @@ -809,18 +700,18 @@ invoke a harness from the current project - `--session-id `: the Runtime session ID to continue (33-100 characters) - `--qualifier `: the harness endpoint qualifier to invoke (default DEFAULT) -#### `agentcore project log` +### `agentcore log` ```text -agentcore project log [options] [command] +agentcore log [options] [command] ``` inspect logs for resources in the current project -##### `agentcore project log runtime` +#### `agentcore log runtime` ```text -agentcore project log runtime [options] +agentcore log runtime [options] ``` stream or search logs for a Runtime in the current project @@ -837,10 +728,10 @@ stream or search logs for a Runtime in the current project - `--query `: CloudWatch Logs filter pattern - `--limit `: maximum number of log records to return in search mode -##### `agentcore project log harness` +#### `agentcore log harness` ```text -agentcore project log harness [options] +agentcore log harness [options] ``` stream or search logs for a Harness in the current project @@ -857,26 +748,26 @@ stream or search logs for a Harness in the current project - `--query `: CloudWatch Logs filter pattern - `--limit `: maximum number of log records to return in search mode -#### `agentcore project traces` +### `agentcore traces` ```text -agentcore project traces [options] [command] +agentcore traces [options] [command] ``` inspect traces for resources in the current project -##### `agentcore project traces runtime` +#### `agentcore traces runtime` ```text -agentcore project traces runtime [options] [command] +agentcore traces runtime [options] [command] ``` inspect a Runtime's traces -###### `agentcore project traces runtime list` +##### `agentcore traces runtime list` ```text -agentcore project traces runtime list [options] +agentcore traces runtime list [options] ``` list a Runtime's recent traces @@ -890,10 +781,10 @@ list a Runtime's recent traces - `--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 project traces runtime get` +##### `agentcore traces runtime get` ```text -agentcore project traces runtime get [options] +agentcore traces runtime get [options] ``` download a trace's log records to a JSON file @@ -911,18 +802,18 @@ download a trace's log records to a JSON file - `--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 project traces harness` +#### `agentcore traces harness` ```text -agentcore project traces harness [options] [command] +agentcore traces harness [options] [command] ``` inspect a Harness's traces -###### `agentcore project traces harness list` +##### `agentcore traces harness list` ```text -agentcore project traces harness list [options] +agentcore traces harness list [options] ``` list a Harness's recent traces @@ -936,10 +827,10 @@ list a Harness's recent traces - `--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 project traces harness get` +##### `agentcore traces harness get` ```text -agentcore project traces harness get [options] +agentcore traces harness get [options] ``` download a Harness trace's log records to a JSON file @@ -957,10 +848,10 @@ download a Harness trace's log records to a JSON file - `--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 project status` +### `agentcore status` ```text -agentcore project status [options] +agentcore status [options] ``` show the status of the project's deployed resources @@ -969,1264 +860,14 @@ show the status of the project's deployed resources - `--target `: name of the aws-targets.json entry to report on (default: "default") -#### `agentcore project build` +### `agentcore build` ```text -agentcore project build [options] +agentcore 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` diff --git a/scripts/generate-command-reference.mjs b/scripts/generate-command-reference.mjs index f60b440a0c..418271312c 100644 --- a/scripts/generate-command-reference.mjs +++ b/scripts/generate-command-reference.mjs @@ -26,12 +26,6 @@ const DEFAULT_GROUPS = [ "build", ], }, - { 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: "evaluation", title: "Evaluation commands", commands: ["eval"] }, { id: "settings", diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index ca90406a3d..66e0ecff06 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,22 +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: [ - "create", - "add", - "remove", - "deploy", - "invoke", - "status", - "build", - "harness", - "identity", - "runtime", - "memory", - "gateway", - "eval", - ], - cliOnly: ["export", "dev", "log", "traces", "payment", "feedback", "config", "update"], + screens: ["create", "add", "remove", "deploy", "invoke", "status", "build", "eval"], + cliOnly: ["export", "dev", "log", "traces", "feedback", "config", "update"], }); r.unmount(); }); @@ -79,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({ @@ -100,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"); @@ -150,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).toContain("harness"); - 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/CliOnlyScreen.tsx b/src/components/CliOnlyScreen.tsx index 05da1255bc..a1af715af7 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/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index 0a0269253a..256b1c7dde 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, + TestCoreClient, + waitFor, + waitForText, +} from "../testing"; afterEach(cleanupScreens); @@ -50,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"); @@ -74,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"); @@ -90,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"); @@ -100,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"); @@ -108,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"); @@ -121,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"); @@ -147,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"); @@ -205,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"); @@ -255,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"); @@ -280,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"); @@ -317,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("/"); @@ -373,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("/"); @@ -400,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"); @@ -431,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(() => { @@ -459,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]) { @@ -486,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/ProjectResourceCreateScreen.test.tsx b/src/components/ProjectResourceCreateScreen.test.tsx index d735f9e3a8..a32294ad19 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..ced018f34b 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, + 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", () => { @@ -56,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/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index f9095c029e..3dd9893295 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -1,5 +1,12 @@ import { test, expect, describe, afterEach } from "bun:test"; -import { cleanupScreens, renderScreen, tick, waitForText } from "../testing"; +import { + cleanupScreens, + menuEntries, + renderScreen, + renderImperativeScreen, + tick, + waitForText, +} from "../testing"; afterEach(cleanupScreens); @@ -10,22 +17,32 @@ 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"); + const entries = menuEntries(frame); + expect(entries.screens).toContain("create"); + expect(frame).toContain("eval"); + for (const family of ["harness", "identity", "runtime", "memory", "gateway", "payment"]) { + expect([...entries.screens, ...entries.cliOnly]).not.toContain(family); + } expect(frame).toContain("config"); expect(frame).toContain("read/write global config values"); 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"); @@ -33,7 +50,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 +62,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 create. expect(r.lastFrame()).toContain("❯ create"); r.unmount(); @@ -54,7 +71,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 +85,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 +94,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"); @@ -87,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"); @@ -120,7 +151,7 @@ describe("navigation", () => { }); 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/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); 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.screen.test.tsx b/src/handlers/gateway/gateway.mutations.screen.test.tsx index 88f4501cb8..342c352f21 100644 --- a/src/handlers/gateway/gateway.mutations.screen.test.tsx +++ b/src/handlers/gateway/gateway.mutations.screen.test.tsx @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { cleanupScreens, menuEntries, renderScreen, waitForText } from "../../testing"; +import { cleanupScreens, menuEntries, renderImperativeScreen, waitForText } from "../../testing"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; afterEach(cleanupScreens); @@ -7,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([]); @@ -19,8 +19,12 @@ 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 }, + const screen = renderImperativeScreen(`/agentcore/${group}`, { + globalConfig: { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": true, + "imperative-commands": true, + }, }); await waitForText(screen.lastFrame, "command line only"); const entries = menuEntries(screen.lastFrame()!); @@ -30,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"); @@ -49,8 +53,12 @@ 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 }, + const screen = renderImperativeScreen("/agentcore/gateway/create", { + globalConfig: { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-mutation-commands": enabled, + "imperative-commands": true, + }, }); await waitForText( screen.lastFrame, @@ -72,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.mutations.test.tsx b/src/handlers/gateway/gateway.mutations.test.tsx index 3e3f51c516..8297e5f2f2 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.screen.test.tsx b/src/handlers/gateway/gateway.screen.test.tsx index fb1995a027..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, - 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,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 = renderImperativeScreen("/agentcore/gateway/list", { + core, + endpointUrl: ENDPOINT, + }); await waitForText(screen.lastFrame, "checkout-gateway"); const frame = screen.lastFrame()!; @@ -145,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"); @@ -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 = renderImperativeScreen("/agentcore/gateway/list", { + core, + endpointUrl: ENDPOINT, + }); await waitForText(screen.lastFrame, "checkout-gateway"); await screen.press("return"); @@ -197,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"); }); @@ -215,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"); @@ -229,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"); @@ -238,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({ @@ -253,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"); @@ -277,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, @@ -309,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"); @@ -322,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"); @@ -341,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({ @@ -356,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"); @@ -375,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 }, ); @@ -398,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 }, ); @@ -408,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)}`, ); @@ -418,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"); @@ -427,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({ @@ -440,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"); @@ -460,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"); @@ -480,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"); @@ -494,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/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.screen.test.tsx b/src/handlers/gateway/invoke/invoke.screen.test.tsx index a0cf2b1d19..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, - 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/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/gateway/policy/generate.screen.test.tsx b/src/handlers/gateway/policy/generate.screen.test.tsx index 9b2621e2b6..d3562df762 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, + TestCoreClient, + waitForText, +} from "../../../testing"; afterEach(cleanupScreens); @@ -33,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), }); @@ -48,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), }); @@ -64,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), }); @@ -86,7 +91,9 @@ describe("gateway policy generate screen", () => { { statement: PERMIT, findings: [] }, ], }; - const screen = renderScreen(`/agentcore/gateway/policy/generate/${GATEWAY_ID}`, { core }); + const screen = renderImperativeScreen(`/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 = renderImperativeScreen(`/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 = renderImperativeScreen(`/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..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 { - 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 843fdc53f0..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 { - 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 454dcd450f..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 { - 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,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 = renderImperativeScreen("/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 = renderImperativeScreen("/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..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 { - 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,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 = renderImperativeScreen("/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 = renderImperativeScreen("/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..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 { - 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,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 = renderImperativeScreen("/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..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 { - 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,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 = renderImperativeScreen("/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 = renderImperativeScreen("/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..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 { - 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/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/get/get.screen.test.tsx b/src/handlers/harness/get/get.screen.test.tsx index 7bf33fd55e..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 { - 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/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.screen.test.tsx b/src/handlers/harness/invoke/invoke.screen.test.tsx index 5591f94d1c..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 { - 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/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/harness/list/list.screen.test.tsx b/src/handlers/harness/list/list.screen.test.tsx index 54701e3843..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 { - 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 40c8517741..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 { - 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 bee5261d52..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 { - 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,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 = renderImperativeScreen("/agentcore/harness/version/list/MyHarness-abc123", { + core, + }); await waitForText(laterPage.lastFrame, "page 1 · more →"); await laterPage.write("l"); @@ -155,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"); @@ -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 = renderImperativeScreen("/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/help.screen.test.tsx b/src/handlers/help.screen.test.tsx index 400dce8822..651730525e 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).toMatch(/^\s+create\s+/m); + expect(output).toContain("eval"); + expect(output).not.toMatch(/^\s+harness\s+/m); expect(output).toContain("config"); }); }); 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..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, - 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,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 = renderImperativeScreen("/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 }); + renderImperativeScreen("/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 = renderImperativeScreen("/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); @@ -119,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(); @@ -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 = renderImperativeScreen("/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 = renderImperativeScreen("/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 = renderImperativeScreen("/agentcore/identity/api-key-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, name); await screen.press("return"); @@ -171,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()!; @@ -194,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"); @@ -210,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"); @@ -229,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/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.screen.test.tsx b/src/handlers/identity/oauth2-credential-provider/oauth2.screen.test.tsx index 4429deb13d..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, - 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,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 = renderImperativeScreen("/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 }); + renderImperativeScreen("/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 = renderImperativeScreen("/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); @@ -122,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(); @@ -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 = renderImperativeScreen("/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 = renderImperativeScreen("/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 = renderImperativeScreen("/agentcore/identity/oauth2-credential-provider/list", { + core, + }); await waitForText(screen.lastFrame, name); await screen.press("return"); @@ -172,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()!; @@ -196,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"); }); @@ -220,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()!; @@ -233,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"); @@ -249,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"); @@ -268,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")); 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/index.tsx b/src/handlers/index.tsx index 6bd9e54f86..7f73e4ff07 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -93,12 +93,14 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router createProjectHandlers(core, io).forEach((handler) => { root.handler(handler); }); - 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/memory/actor/actor.screen.test.tsx b/src/handlers/memory/actor/actor.screen.test.tsx index 219130a951..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, - 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 3797dd76bd..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, - 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 aed4db7b3e..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, - 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/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/memory/record/record.screen.test.tsx b/src/handlers/memory/record/record.screen.test.tsx index 8fea5d8660..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, - 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 e431689130..3c995d2695 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, + 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 = renderImperativeScreen(`/agentcore/memory/session/list/${memoryId}/${actorId}`, { + core, + }); await waitForText(screen.lastFrame, sessionId); const frame = screen.lastFrame()!; @@ -52,13 +59,15 @@ 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", { core }); + const failed = renderImperativeScreen("/agentcore/memory/session/list/memory-1/actor-1", { + core, + }); await waitForText(failed.lastFrame, "sessions unavailable"); core.memory.setError(undefined); 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 { await waitForText(screen.lastFrame, "the platform for production AI agents"); // Past the screen that pinned, the menus and what they open fetch in the // launch region again. - 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 57db288fd3..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,28 +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", - "harness", - "identity", - "runtime", - "memory", - "gateway", - "payment", - "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'`, + ); }); }); diff --git a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx index 05ee4c9c14..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, - 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 a40b2b5df7..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, - 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,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 = renderImperativeScreen(`/agentcore/runtime/invoke/${runtimeId}/${QUALIFIER}`, { + core, + }); await waitForText(screen.lastFrame, "Ready"); await screen.resize(80, 24); @@ -371,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}'); @@ -403,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("{}"); @@ -435,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("{}"); @@ -470,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("{}"); @@ -486,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("{}"); @@ -508,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("{}"); @@ -528,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("{}"); @@ -550,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("{}"); @@ -572,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"); @@ -605,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, { @@ -651,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, { @@ -704,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, { @@ -780,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, { @@ -824,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("{}"); @@ -851,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("{}"); @@ -870,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("{}"); @@ -898,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/invoke/invoke.test.tsx b/src/handlers/runtime/invoke/invoke.test.tsx index 909e82eb35..db1022a821 100644 --- a/src/handlers/runtime/invoke/invoke.test.tsx +++ b/src/handlers/runtime/invoke/invoke.test.tsx @@ -6,6 +6,7 @@ import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore- import type { AppIO } from "../../../io"; import type { RuntimeInvokeRequest } from "../types"; import { + IMPERATIVE_GLOBAL_CONFIG, createSilentLogger, expectError, TestCoreClient, @@ -63,9 +64,12 @@ function failingStdoutIO(): AppIO { 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]); } 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.screen.test.tsx b/src/handlers/runtime/runtime.screen.test.tsx index 0500d70c11..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, - 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,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 = renderImperativeScreen("/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 = renderImperativeScreen("/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/); @@ -212,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()!; @@ -236,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"); @@ -276,7 +280,7 @@ describe("runtime hub", () => { ], }); } - const r = renderScreen(`/agentcore/runtime/get/${encodeURIComponent(runtimeId)}`, { + const r = renderImperativeScreen(`/agentcore/runtime/get/${encodeURIComponent(runtimeId)}`, { core, }); @@ -306,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"); @@ -329,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"); @@ -351,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, }); @@ -373,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"); @@ -389,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"); @@ -401,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"); @@ -434,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"); @@ -460,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"); @@ -474,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"); @@ -485,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/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.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index 4989db37ac..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, - 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/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/handlers/runtime/version/version.screen.test.tsx b/src/handlers/runtime/version/version.screen.test.tsx index 53d801b68f..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, - 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); 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); 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, diff --git a/src/tui/tui.test.tsx b/src/tui/tui.test.tsx index 47762ab56e..cc43f9548e 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).toMatch(/^\s+create\s+/m); + expect(out).not.toMatch(/^\s+harness\s+/m); }); 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");