diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index 66e0ecff06..905964367a 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -13,13 +13,11 @@ import { isTuiCommandSupported } from "../router"; afterEach(cleanupScreens); -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 // Commander's own, not one of ours. function cliOnlyCommands( - command = compiledRootCommand(undefined, MUTATION_CONFIG), + command = compiledRootCommand(undefined, IMPERATIVE_GLOBAL_CONFIG), path: string[] = [], ): [string[], Command][] { const here = [...path, command.name()]; @@ -103,7 +101,7 @@ describe("every command-line-only command opens on screen", () => { test.each(CLI_ONLY.map(([path, command]) => [path.join(" "), path, command] as const))( "%s opens its menu or help, and esc returns to the parent", async (_label, path, command) => { - const r = renderScreen("/" + path.join("/"), { globalConfig: MUTATION_CONFIG }); + const r = renderScreen("/" + path.join("/"), { globalConfig: IMPERATIVE_GLOBAL_CONFIG }); // Wide and tall enough that no option term wraps and nothing is below the // fold; scrolling and wrapping have their own tests. await r.resize(220, 200); @@ -151,7 +149,7 @@ describe("paths without a screen of their own", () => { ); test("a group drills down to a leaf's help and back", async () => { - const r = renderScreen("/agentcore/gateway", { globalConfig: MUTATION_CONFIG }); + const r = renderScreen("/agentcore/gateway", { globalConfig: IMPERATIVE_GLOBAL_CONFIG }); await waitForText(r.lastFrame, "command line only"); await r.write("create"); @@ -196,7 +194,7 @@ describe("option help groups", () => { }); test("a command whose flags carry no group keeps a single options section", async () => { - const r = renderScreen("/agentcore/gateway/create", { globalConfig: MUTATION_CONFIG }); + const r = renderScreen("/agentcore/gateway/create", { globalConfig: IMPERATIVE_GLOBAL_CONFIG }); await waitForText(r.lastFrame, "this command runs from the command line"); const frame = r.lastFrame()!; diff --git a/src/components/ProjectResourceCreateScreen.test.tsx b/src/components/ProjectResourceCreateScreen.test.tsx index a32294ad19..2471256136 100644 --- a/src/components/ProjectResourceCreateScreen.test.tsx +++ b/src/components/ProjectResourceCreateScreen.test.tsx @@ -11,8 +11,6 @@ import type { ProjectCreateResource } from "./ProjectResourceCreateScreen"; afterEach(cleanupScreens); -const MUTATION_CONFIG = { ...IMPERATIVE_GLOBAL_CONFIG, "imperative-mutation-commands": true }; - const RESOURCES = [ { resource: "runtime", @@ -26,12 +24,6 @@ const RESOURCES = [ parentDescription: "inspect AgentCore Memories", addCommand: "agentcore add memory", }, - { - resource: "gateway", - label: "Gateway", - parentDescription: "manage AgentCore Gateways", - addCommand: "agentcore add gateway --name MyGateway", - }, ] as const satisfies { resource: ProjectCreateResource; label: string; @@ -57,6 +49,7 @@ describe("project resource creation guidance", () => { expect(frame).toContain(addCommand); expect(frame).toContain("agentcore deploy"); expect(frame).not.toContain("┌"); + expect(frame).not.toContain("this command runs from the command line"); await r.press("escape"); await waitForText(r.lastFrame, parentDescription); @@ -71,26 +64,4 @@ describe("project resource creation guidance", () => { expect(command?.commands.some((candidate) => candidate.name() === "create")).toBe(false); } }); - - test.each(RESOURCES.filter(({ resource }) => resource !== "gateway"))( - "$resource keeps project guidance when Gateway mutations are enabled", - async ({ resource, label, addCommand }) => { - 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"); - expect(entries.cliOnly).not.toContain("create"); - - await r.press("return"); - await waitForText(r.lastFrame, `Create an AgentCore ${label}`); - expect(r.lastFrame()).toContain(addCommand); - expect(r.lastFrame()).not.toContain("this command runs from the command line"); - - const command = compiledRootCommand(undefined, MUTATION_CONFIG).commands.find( - (candidate) => candidate.name() === resource, - ); - expect(command?.commands.some((candidate) => candidate.name() === "create")).toBe(false); - r.unmount(); - }, - ); }); diff --git a/src/globalConfig/config.tsx b/src/globalConfig/config.tsx index 8ef14b63f7..c257840a4a 100644 --- a/src/globalConfig/config.tsx +++ b/src/globalConfig/config.tsx @@ -5,7 +5,6 @@ import type { DeepPartial, GlobalConfig } from "./types"; */ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { "imperative-commands": false, - "imperative-mutation-commands": false, telemetry: { enabled: true, audit: false, @@ -24,8 +23,6 @@ export function applyOverrides( ): GlobalConfig { return { "imperative-commands": overrides["imperative-commands"] ?? defaults["imperative-commands"], - "imperative-mutation-commands": - overrides["imperative-mutation-commands"] ?? defaults["imperative-mutation-commands"], telemetry: { enabled: overrides.telemetry?.enabled ?? defaults.telemetry.enabled, audit: overrides.telemetry?.audit ?? defaults.telemetry.audit, diff --git a/src/globalConfig/types.tsx b/src/globalConfig/types.tsx index a6b3d3823f..cf215031b1 100644 --- a/src/globalConfig/types.tsx +++ b/src/globalConfig/types.tsx @@ -13,7 +13,6 @@ export type DeepPartial = { [P in keyof T]?: DeepPartial }; */ export const globalConfigFileSchema = z.object({ "imperative-commands": z.boolean().optional(), - "imperative-mutation-commands": z.boolean().optional(), telemetry: z .object({ enabled: z.boolean().optional(), diff --git a/src/handlers/config/config.test.tsx b/src/handlers/config/config.test.tsx index a0cc534b38..e55c75132f 100644 --- a/src/handlers/config/config.test.tsx +++ b/src/handlers/config/config.test.tsx @@ -1,6 +1,6 @@ import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { join } from "node:path"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { createRootHandler } from "../index"; import { createSilentLogger, TestCoreClient, testIO } from "../../testing"; @@ -62,13 +62,10 @@ describe("config", () => { 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); + test("persists the imperative flag across config accessor instances", async () => { 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); } }); @@ -86,35 +83,44 @@ describe("config", () => { 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); - expect(JSON.parse(await run(["imperative-mutation-commands"]))).toBe(false); - }); - - test("persists the mutation flag across config accessor instances", async () => { - for (const enabled of [true, false]) { - expect(JSON.parse(await run(["imperative-mutation-commands", String(enabled)]))).toBe( - enabled, - ); - expect(JSON.parse(await run(["imperative-mutation-commands"]))).toBe(enabled); - } - }); - - test("rejects an invalid mutation flag without changing the saved value", async () => { - await run(["imperative-mutation-commands", "true"]); - await expect(run(["imperative-mutation-commands", "banana"])).rejects.toThrow( + test.each([false, true, "true"])( + "ignores a retired mutation flag value of %s in an existing config", + async (legacyValue) => { + for (const enabled of [false, true]) { + await writeFile( + configPath, + JSON.stringify({ + ...validConfigOverrides, + "imperative-mutation-commands": legacyValue, + "imperative-commands": enabled, + }), + ); + const config = JSON.parse(await run([])); + expect(config["imperative-commands"]).toBe(enabled); + expect(config).not.toHaveProperty("imperative-mutation-commands"); + expect(config).toMatchObject(validConfigOverrides); + } + }, + ); + + test("rejects reading or setting the retired mutation flag", async () => { + await expect(run(["imperative-mutation-commands"])).rejects.toThrow(InputValidationError); + await expect(run(["imperative-mutation-commands", "true"])).rejects.toThrow( InputValidationError, ); - expect(JSON.parse(await run(["imperative-mutation-commands"]))).toBe(true); + expect(JSON.parse(await run(["imperative-commands"]))).toBe(false); }); - test("rejects a non-boolean mutation flag read from disk", async () => { + test("drops the retired flag when saving another setting", async () => { await writeFile( configPath, - JSON.stringify({ ...validConfigOverrides, "imperative-mutation-commands": "true" }), + JSON.stringify({ ...validConfigOverrides, "imperative-mutation-commands": true }), ); - await expect(run([])).rejects.toThrow("Failed to deserialize"); + await run(["imperative-commands", "true"]); + const saved = JSON.parse(await readFile(configPath, "utf8")); + expect(saved).not.toHaveProperty("imperative-mutation-commands"); + expect(saved["imperative-commands"]).toBe(true); + expect(JSON.parse(await run(["telemetry.endpoint"]))).toBe("https://example.com"); }); test("prints a nested object when a branch key is passed", async () => { diff --git a/src/handlers/gateway/connector/index.tsx b/src/handlers/gateway/connector/index.tsx index 7760270a9b..9594d80c4f 100644 --- a/src/handlers/gateway/connector/index.tsx +++ b/src/handlers/gateway/connector/index.tsx @@ -8,22 +8,13 @@ import { createGetGatewayConnectorHandler } from "./get"; import { createListGatewayConnectorsHandler } from "./list"; import { createUpdateGatewayConnectorHandler } from "./update"; -export function createGatewayConnectorHandler( - core: Core, - io: AppIO, - imperativeMutationCommands = false, -): Router { - const router = new Router("connector", "manage connectors configured for an AgentCore Gateway") +export function createGatewayConnectorHandler(core: Core, io: AppIO): Router { + return new Router("connector", "manage connectors configured for an AgentCore Gateway") .default(renderTui(core, io)) - .supportedTuiCommands("get", "list"); - if (imperativeMutationCommands) { - router - .handler(createCreateGatewayConnectorHandler(core, io)) - .handler(createUpdateGatewayConnectorHandler(core, io)); - } - router + .supportedTuiCommands("get", "list") + .handler(createCreateGatewayConnectorHandler(core, io)) + .handler(createUpdateGatewayConnectorHandler(core, io)) .handler(createGetGatewayConnectorHandler(core)) - .handler(createListGatewayConnectorsHandler(core)); - if (imperativeMutationCommands) router.handler(createDeleteGatewayConnectorHandler(core)); - return router; + .handler(createListGatewayConnectorsHandler(core)) + .handler(createDeleteGatewayConnectorHandler(core)); } diff --git a/src/handlers/gateway/create/screen.tsx b/src/handlers/gateway/create/screen.tsx index 731c39fcbd..ebfd6dedd4 100644 --- a/src/handlers/gateway/create/screen.tsx +++ b/src/handlers/gateway/create/screen.tsx @@ -7,7 +7,10 @@ import type { ScreenProps } from "../../types"; export function GatewayCreateScreen(props: ScreenProps) { const gateway = resolveCommand(props.ctx.require(CommandKey), ["agentcore", "gateway"]); const path = ["agentcore", "gateway", "create"]; - if (gateway.commands.some((command) => command.name() === "create")) { + if ( + gateway.name() === "gateway" && + gateway.commands.some((command) => command.name() === "create") + ) { return ; } diff --git a/src/handlers/gateway/gateway.create.test.tsx b/src/handlers/gateway/gateway.create.test.tsx index 04c0c16456..dcbdb445ed 100644 --- a/src/handlers/gateway/gateway.create.test.tsx +++ b/src/handlers/gateway/gateway.create.test.tsx @@ -18,6 +18,7 @@ import { CoreClient } from "../../core"; import { createControlClient, createIamClient } from "../../core/factories"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, expectError, fixtureFactories, isRecording, @@ -27,13 +28,6 @@ import { } from "../../testing"; import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; -import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; - -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"; @@ -76,8 +70,10 @@ async function run(args: string[]): Promise { const root = createRootHandler(createFixtureCore(), { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", REGION]); return io.stdout(); diff --git a/src/handlers/gateway/gateway.delete.test.tsx b/src/handlers/gateway/gateway.delete.test.tsx index 19424a4a13..f537a09d26 100644 --- a/src/handlers/gateway/gateway.delete.test.tsx +++ b/src/handlers/gateway/gateway.delete.test.tsx @@ -26,6 +26,7 @@ import { CoreClient } from "../../core"; import { createControlClient, createIamClient } from "../../core/factories"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, expectError, fixtureFactories, isRecording, @@ -36,13 +37,6 @@ import { } from "../../testing"; import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; -import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; - -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"; @@ -56,8 +50,10 @@ async function run( const root = createRootHandler(core, { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -235,8 +231,10 @@ async function runFixture(args: string[]): Promise { const root = createRootHandler(createFixtureCore(), { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", "us-east-1"]); return io.stdout(); diff --git a/src/handlers/gateway/gateway.mutations.screen.test.tsx b/src/handlers/gateway/gateway.mutations.screen.test.tsx index 342c352f21..8e499f8049 100644 --- a/src/handlers/gateway/gateway.mutations.screen.test.tsx +++ b/src/handlers/gateway/gateway.mutations.screen.test.tsx @@ -1,30 +1,33 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { cleanupScreens, menuEntries, renderImperativeScreen, waitForText } from "../../testing"; +import { + cleanupScreens, + IMPERATIVE_GLOBAL_CONFIG, + menuEntries, + renderScreen, + waitForText, +} from "../../testing"; import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; afterEach(cleanupScreens); 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 = renderImperativeScreen(`/agentcore/${group}`); + test.each(GROUPS)("%s redirects to root when the parent flag is off", async (group) => { + const screen = renderScreen(`/agentcore/${group}`); + await waitForText(screen.lastFrame, "the platform for production AI agents"); await waitForText(screen.lastFrame, "type to choose a command"); const entries = menuEntries(screen.lastFrame()!); - expect(entries.cliOnly).toEqual([]); - expect(screen.lastFrame()).not.toContain("command line only"); - expect(entries.screens).not.toContain("update"); - expect(entries.screens).not.toContain("delete"); - expect(entries.screens.includes("create")).toBe(group === "gateway"); + expect(entries.screens).not.toContain("gateway"); + expect(entries.cliOnly).not.toContain("create"); + expect(entries.cliOnly).not.toContain("delete"); + expect(screen.frames.join("\n")).not.toContain("manage AgentCore Gateways"); + expect(screen.frames.join("\n")).not.toContain("this command runs from the command line"); expect(screen.core.gateway.calls).toEqual([]); }); test.each(GROUPS)("%s preserves enabled CLI-only mutations", async (group) => { - const screen = renderImperativeScreen(`/agentcore/${group}`, { - globalConfig: { - ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": true, - "imperative-commands": true, - }, + const screen = renderScreen(`/agentcore/${group}`, { + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await waitForText(screen.lastFrame, "command line only"); const entries = menuEntries(screen.lastFrame()!); @@ -33,11 +36,8 @@ describe("Gateway mutation menus", () => { expect(screen.core.gateway.calls).toEqual([]); }); - test("disabled create opens project guidance and returns to the Gateway menu", async () => { - 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"); + test("disabled direct create opens project guidance and returns to root", async () => { + const screen = renderScreen("/agentcore/gateway/create"); await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); const frame = screen.lastFrame()!; expect(frame).toContain("agentcore create"); @@ -48,17 +48,13 @@ describe("Gateway mutation menus", () => { expect(frame).not.toContain("this command runs from the command line"); expect(screen.core.gateway.calls).toEqual([]); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage AgentCore Gateways"); - expect(menuEntries(screen.lastFrame()!).cliOnly).toEqual([]); + await waitForText(screen.lastFrame, "the platform for production AI agents"); + expect(menuEntries(screen.lastFrame()!).screens).not.toContain("gateway"); }); - test.each([false, true])("direct create route matches flag %s", async (enabled) => { - const screen = renderImperativeScreen("/agentcore/gateway/create", { - globalConfig: { - ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": enabled, - "imperative-commands": true, - }, + test.each([false, true])("direct create route matches parent flag %s", async (enabled) => { + const screen = renderScreen("/agentcore/gateway/create", { + globalConfig: enabled ? IMPERATIVE_GLOBAL_CONFIG : DEFAULT_GLOBAL_CONFIG, }); await waitForText( screen.lastFrame, @@ -67,10 +63,16 @@ describe("Gateway mutation menus", () => { expect(screen.lastFrame()?.includes("agentcore add gateway")).toBe(!enabled); expect(screen.lastFrame()?.includes("--authorizer-type")).toBe(enabled); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage AgentCore Gateways"); - expect(menuEntries(screen.lastFrame()!).cliOnly).toEqual( - enabled ? ["create", "update", "delete"] : [], + expect(screen.core.gateway.calls).toEqual([]); + await waitForText( + screen.lastFrame, + enabled ? "manage AgentCore Gateways" : "the platform for production AI agents", ); + if (enabled) { + expect(menuEntries(screen.lastFrame()!).cliOnly).toEqual(["create", "update", "delete"]); + } else { + expect(menuEntries(screen.lastFrame()!).screens).not.toContain("gateway"); + } }); test.each( @@ -80,14 +82,15 @@ describe("Gateway mutation menus", () => { .map((mutation) => `${group}/${mutation}`), ), )("disabled direct route %s cannot expose mutation help", async (path) => { - const screen = renderImperativeScreen(`/agentcore/${path}`); + const screen = renderScreen(`/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.frames.join("\n")).not.toContain(`agentcore ${path.replaceAll("/", " ")}`); expect(screen.core.gateway.calls).toEqual([]); }); test("project guidance remains scrollable after resizing a small terminal", async () => { - const screen = renderImperativeScreen("/agentcore/gateway/create"); + const screen = renderScreen("/agentcore/gateway/create"); await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); await screen.resize(50, 12); await screen.write("\u001b[6~"); @@ -96,6 +99,6 @@ describe("Gateway mutation menus", () => { await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); expect(screen.lastFrame()).toContain("agentcore deploy"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage AgentCore Gateways"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); }); diff --git a/src/handlers/gateway/gateway.mutations.test.tsx b/src/handlers/gateway/gateway.mutations.test.tsx index 8297e5f2f2..f66debbd50 100644 --- a/src/handlers/gateway/gateway.mutations.test.tsx +++ b/src/handlers/gateway/gateway.mutations.test.tsx @@ -19,11 +19,10 @@ function setup(enabled?: boolean) { const io = testIO(); const globalConfig = enabled === undefined - ? IMPERATIVE_GLOBAL_CONFIG + ? DEFAULT_GLOBAL_CONFIG : { ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": enabled, - "imperative-commands": true, + "imperative-commands": enabled, }; const root = createRootHandler(core, { io: io.io, @@ -37,16 +36,15 @@ function setup(enabled?: boolean) { } describe("Gateway imperative mutation availability", () => { - test("Gateway reads command availability from the resolved config", () => { - for (const enabled of [false, true]) { - 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()); + test("Gateway factory registers all mutations without a separate gate", () => { + const gateway = createGatewayHandler(new TestCoreClient(), testIO().io); + for (const path of GROUPS) { + const group = path.length + ? gateway.children().find((child) => child.name() === path[0])! + : gateway; + const names = group.children().map((child) => child.name()); for (const mutation of MUTATIONS) { - expect(names.includes(mutation)).toBe(enabled); + expect(names).toContain(mutation); } } }); @@ -62,30 +60,29 @@ describe("Gateway imperative mutation availability", () => { io: testIO().io, logger: createSilentLogger(), globalConfigAccessor, - globalConfig: { - ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": true, - "imperative-commands": true, - }, + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); const gateway = root.children().find((child) => child.name() === "gateway")!; expect(gateway.children().map((child) => child.name())).toContain("create"); expect(reads).toBe(0); }); - test.each([undefined, false, true])("builds help and commands for flag %s", (enabled) => { + test.each([undefined, false, true])("builds help and commands for parent flag %s", (enabled) => { const { command } = setup(enabled); - const gateway = command.commands.find((child) => child.name() === "gateway")!; + const gateway = command.commands.find((child) => child.name() === "gateway"); + expect(Boolean(gateway)).toBe(enabled === true); + expect(/\n\s+gateway\s/.test(command.helpInformation())).toBe(enabled === true); + const add = command.commands.find((child) => child.name() === "add")!; + expect(add.commands.map((child) => child.name())).toContain("gateway"); + if (!gateway) return; for (const path of GROUPS) { const group = path.length ? gateway.commands.find((child) => child.name() === path[0])! : gateway; const names = group.commands.map((child) => child.name()); for (const mutation of MUTATIONS) { - expect(names.includes(mutation)).toBe(enabled === true); - expect(new RegExp(`\\n\\s+${mutation}\\s`).test(group.helpInformation())).toBe( - enabled === true, - ); + expect(names).toContain(mutation); + expect(group.helpInformation()).toMatch(new RegExp(`\\n\\s+${mutation}\\s`)); } expect(names).toContain("get"); expect(names).toContain("list"); @@ -94,8 +91,6 @@ describe("Gateway imperative mutation availability", () => { expect(gateway.commands.find((child) => child.name() === "policy")?.commands[0]?.name()).toBe( "generate", ); - const add = command.commands.find((child) => child.name() === "add")!; - expect(add.commands.map((child) => child.name())).toContain("gateway"); const harness = command.commands.find((child) => child.name() === "harness")!; expect(harness.commands.map((child) => child.name())).toContain("create"); }); diff --git a/src/handlers/gateway/gateway.screen.test.tsx b/src/handlers/gateway/gateway.screen.test.tsx index e1b0a4afc2..99dadf5b3a 100644 --- a/src/handlers/gateway/gateway.screen.test.tsx +++ b/src/handlers/gateway/gateway.screen.test.tsx @@ -116,8 +116,8 @@ describe("Gateway menu and list", () => { await waitForText(screen.lastFrame, "manage AgentCore Gateways"); expect(menuEntries(screen.lastFrame()!)).toEqual({ - screens: ["create", "get", "list", "invoke", "target", "connector", "rule", "policy"], - cliOnly: [], + screens: ["get", "list", "invoke", "target", "connector", "rule", "policy"], + cliOnly: ["create", "update", "delete"], }); expect(screen.core.gateway.calls).toEqual([]); }); @@ -258,7 +258,7 @@ describe("Gateway Target flow", () => { await waitForText(screen.lastFrame, "manage Targets for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ screens: ["get", "list"], - cliOnly: [], + cliOnly: ["create", "update", "delete"], }); expect(screen.core.gateway.calls).toEqual([]); }); @@ -361,7 +361,7 @@ describe("Gateway Connector flow", () => { await waitForText(screen.lastFrame, "manage connectors configured for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ screens: ["get", "list"], - cliOnly: [], + cliOnly: ["create", "update", "delete"], }); expect(screen.core.gateway.calls).toEqual([]); }); @@ -447,7 +447,7 @@ describe("Gateway Rule flow", () => { await waitForText(screen.lastFrame, "manage Rules for an AgentCore Gateway"); expect(menuEntries(screen.lastFrame()!)).toEqual({ screens: ["get", "list"], - cliOnly: [], + cliOnly: ["create", "update", "delete"], }); expect(screen.core.gateway.calls).toEqual([]); }); diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index 6b3f50e6fa..4728cf85a2 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -9,6 +9,7 @@ import type { AwsClients } from "../../core/types"; import { NetworkingError, UserCancellationError, InputValidationError } from "../../errors"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, expectError, TestCoreClient, TestGlobalConfigAccessor, @@ -20,13 +21,6 @@ import { PathKey } from "../../router"; import { JsonKey } from "../keys"; 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, - "imperative-commands": true, -}; const REGION = "us-west-2"; const GATEWAY_ID = "gateway-1"; const TARGET_ID = "target-1"; @@ -40,8 +34,10 @@ async function run( const root = createRootHandler(core, { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", REGION]); @@ -53,8 +49,10 @@ function supportsTui(path: readonly string[]): boolean { createRootHandler(new TestCoreClient(), { io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }), ValueContext.EmptyContext(), ); @@ -72,8 +70,10 @@ describe("gateway command hierarchy", () => { const root = createRootHandler(new TestCoreClient(), { io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); const gateway = root.children().find((child) => child.name() === "gateway"); const target = gateway?.children().find((child) => child.name() === "target"); diff --git a/src/handlers/gateway/gateway.update.test.tsx b/src/handlers/gateway/gateway.update.test.tsx index d4e857ebbd..a56748b61b 100644 --- a/src/handlers/gateway/gateway.update.test.tsx +++ b/src/handlers/gateway/gateway.update.test.tsx @@ -22,6 +22,7 @@ import { CoreClient } from "../../core"; import { createControlClient, createIamClient } from "../../core/factories"; import { createSilentLogger, + IMPERATIVE_GLOBAL_CONFIG, expectError, fixtureFactories, isRecording, @@ -32,13 +33,6 @@ import { } from "../../testing"; import { createRootHandler } from "../index"; import { InputValidationError } from "../../errors"; -import { DEFAULT_GLOBAL_CONFIG } from "../../globalConfig"; - -const MUTATION_CONFIG = { - ...DEFAULT_GLOBAL_CONFIG, - "imperative-mutation-commands": true, - "imperative-commands": true, -}; async function runWithTestCore(args: string[]): Promise { const core = new TestCoreClient(); @@ -46,8 +40,10 @@ async function runWithTestCore(args: string[]): Promise { const root = createRootHandler(core, { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", "us-west-2"]); return core; @@ -58,8 +54,10 @@ describe("Gateway update command hierarchy", () => { const root = createRootHandler(new TestCoreClient(), { io: testIO().io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); const gateway = root.children().find((child) => child.name() === "gateway")!; @@ -299,8 +297,10 @@ async function runFixture(args: string[]): Promise { const root = createRootHandler(createFixtureCore(), { io: io.io, logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor({ initialConfigData: MUTATION_CONFIG }), - globalConfig: MUTATION_CONFIG, + globalConfigAccessor: new TestGlobalConfigAccessor({ + initialConfigData: IMPERATIVE_GLOBAL_CONFIG, + }), + globalConfig: IMPERATIVE_GLOBAL_CONFIG, }); await root.route(["node", "agentcore", ...args, "--region", REGION]); return io.stdout(); diff --git a/src/handlers/gateway/index.tsx b/src/handlers/gateway/index.tsx index 98e3977649..36eb302051 100644 --- a/src/handlers/gateway/index.tsx +++ b/src/handlers/gateway/index.tsx @@ -1,5 +1,4 @@ import { renderTui } from "../../tui"; -import type { GlobalConfig } from "../../globalConfig"; import type { AppIO } from "../../io"; import { withTuiOnEmptyFlagsAndArgs } from "../../middleware"; import { Router } from "../../router"; @@ -15,23 +14,19 @@ import { createGatewayRuleHandler } from "./rule"; import { createGatewayTargetHandler } from "./target"; import { createUpdateGatewayHandler } from "./update"; -export function createGatewayHandler(core: Core, io: AppIO, globalConfig: GlobalConfig): Router { - const imperativeMutationCommands = globalConfig["imperative-mutation-commands"]; - const router = new Router("gateway", "manage AgentCore Gateways") +export function createGatewayHandler(core: Core, io: AppIO): Router { + return new Router("gateway", "manage AgentCore Gateways") .use(withTuiOnEmptyFlagsAndArgs(core, io)) .default(renderTui(core, io)) - .supportedTuiCommands("get", "list", "invoke", "target", "connector", "rule", "policy"); - if (imperativeMutationCommands) { - router - .handler(createCreateGatewayHandler(core, io)) - .handler(createUpdateGatewayHandler(core, io)); - } - router.handler(createGetGatewayHandler(core)).handler(createListGatewaysHandler(core)); - if (imperativeMutationCommands) router.handler(createDeleteGatewayHandler(core)); - return router + .supportedTuiCommands("get", "list", "invoke", "target", "connector", "rule", "policy") + .handler(createCreateGatewayHandler(core, io)) + .handler(createUpdateGatewayHandler(core, io)) + .handler(createGetGatewayHandler(core)) + .handler(createListGatewaysHandler(core)) + .handler(createDeleteGatewayHandler(core)) .handler(createInvokeGatewayHandler(core, io)) - .handler(createGatewayTargetHandler(core, io, imperativeMutationCommands)) - .handler(createGatewayConnectorHandler(core, io, imperativeMutationCommands)) - .handler(createGatewayRuleHandler(core, io, imperativeMutationCommands)) + .handler(createGatewayTargetHandler(core, io)) + .handler(createGatewayConnectorHandler(core, io)) + .handler(createGatewayRuleHandler(core, io)) .handler(createGatewayPolicyHandler(core, io)); } diff --git a/src/handlers/gateway/rule/index.tsx b/src/handlers/gateway/rule/index.tsx index eafe32c86a..c634c3ff0b 100644 --- a/src/handlers/gateway/rule/index.tsx +++ b/src/handlers/gateway/rule/index.tsx @@ -8,20 +8,13 @@ import { createGetGatewayRuleHandler } from "./get"; import { createListGatewayRulesHandler } from "./list"; import { createUpdateGatewayRuleHandler } from "./update"; -export function createGatewayRuleHandler( - core: Core, - io: AppIO, - imperativeMutationCommands = false, -): Router { - const router = new Router("rule", "manage Rules for an AgentCore Gateway") +export function createGatewayRuleHandler(core: Core, io: AppIO): Router { + return new Router("rule", "manage Rules for an AgentCore Gateway") .default(renderTui(core, io)) - .supportedTuiCommands("get", "list"); - if (imperativeMutationCommands) { - router - .handler(createCreateGatewayRuleHandler(core, io)) - .handler(createUpdateGatewayRuleHandler(core, io)); - } - router.handler(createGetGatewayRuleHandler(core)).handler(createListGatewayRulesHandler(core)); - if (imperativeMutationCommands) router.handler(createDeleteGatewayRuleHandler(core)); - return router; + .supportedTuiCommands("get", "list") + .handler(createCreateGatewayRuleHandler(core, io)) + .handler(createUpdateGatewayRuleHandler(core, io)) + .handler(createGetGatewayRuleHandler(core)) + .handler(createListGatewayRulesHandler(core)) + .handler(createDeleteGatewayRuleHandler(core)); } diff --git a/src/handlers/gateway/screen.tsx b/src/handlers/gateway/screen.tsx index c66ecb2afb..74b3c65f0f 100644 --- a/src/handlers/gateway/screen.tsx +++ b/src/handlers/gateway/screen.tsx @@ -1,11 +1,6 @@ -import { projectCreateTuiCommand } from "../../components/ProjectResourceCreateScreen"; import { RouterScreen } from "../../components/RouterScreen"; import type { ScreenProps } from "../types"; -const TUI_ONLY_COMMANDS = [projectCreateTuiCommand("gateway")]; - export function GatewayScreen(props: ScreenProps) { - return ( - - ); + return ; } diff --git a/src/handlers/gateway/target/index.tsx b/src/handlers/gateway/target/index.tsx index 776c2c41b5..985839dd36 100644 --- a/src/handlers/gateway/target/index.tsx +++ b/src/handlers/gateway/target/index.tsx @@ -8,22 +8,13 @@ import { createGetGatewayTargetHandler } from "./get"; import { createListGatewayTargetsHandler } from "./list"; import { createUpdateGatewayTargetHandler } from "./update"; -export function createGatewayTargetHandler( - core: Core, - io: AppIO, - imperativeMutationCommands = false, -): Router { - const router = new Router("target", "manage Targets for an AgentCore Gateway") +export function createGatewayTargetHandler(core: Core, io: AppIO): Router { + return new Router("target", "manage Targets for an AgentCore Gateway") .default(renderTui(core, io)) - .supportedTuiCommands("get", "list"); - if (imperativeMutationCommands) { - router - .handler(createCreateGatewayTargetHandler(core, io)) - .handler(createUpdateGatewayTargetHandler(core, io)); - } - router + .supportedTuiCommands("get", "list") + .handler(createCreateGatewayTargetHandler(core, io)) + .handler(createUpdateGatewayTargetHandler(core, io)) .handler(createGetGatewayTargetHandler(core)) - .handler(createListGatewayTargetsHandler(core)); - if (imperativeMutationCommands) router.handler(createDeleteGatewayTargetHandler(core)); - return router; + .handler(createListGatewayTargetsHandler(core)) + .handler(createDeleteGatewayTargetHandler(core)); } diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index 7f73e4ff07..964c3d1c46 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -98,7 +98,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router root.handler(createIdentityHandler(core, io)); root.handler(createRuntimeHandler(core, io)); root.handler(createMemoryHandler(core, io)); - root.handler(createGatewayHandler(core, io, globalConfig)); + root.handler(createGatewayHandler(core, io)); root.handler(createPaymentHandler(core, io)); } root.handler(createEvalHandler(core, io)); diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index 4d349954bf..7a27dd5915 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -39,31 +39,22 @@ describe("createRootHandler", () => { 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([false, true])("registers standalone commands for imperative flag %s", (enabled) => { + const command = compiledRootCommand(undefined, { + ...DEFAULT_GLOBAL_CONFIG, + "imperative-commands": enabled, + }); + 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();