Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/components/CliOnlyScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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()!;
Expand Down
31 changes: 1 addition & 30 deletions src/components/ProjectResourceCreateScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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();
},
);
});
3 changes: 0 additions & 3 deletions src/globalConfig/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/globalConfig/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
*/
export const globalConfigFileSchema = z.object({
"imperative-commands": z.boolean().optional(),
"imperative-mutation-commands": z.boolean().optional(),
telemetry: z
.object({
enabled: z.boolean().optional(),
Expand Down
60 changes: 33 additions & 27 deletions src/handlers/config/config.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
});

Expand All @@ -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 () => {
Expand Down
23 changes: 7 additions & 16 deletions src/handlers/gateway/connector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
5 changes: 4 additions & 1 deletion src/handlers/gateway/create/screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CliOnlyScreen {...props} path={path} />;
}

Expand Down
14 changes: 5 additions & 9 deletions src/handlers/gateway/gateway.create.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CoreClient } from "../../core";
import { createControlClient, createIamClient } from "../../core/factories";
import {
createSilentLogger,
IMPERATIVE_GLOBAL_CONFIG,
expectError,
fixtureFactories,
isRecording,
Expand All @@ -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";
Expand Down Expand Up @@ -76,8 +70,10 @@ async function run(args: string[]): Promise<string> {
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();
Expand Down
20 changes: 9 additions & 11 deletions src/handlers/gateway/gateway.delete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CoreClient } from "../../core";
import { createControlClient, createIamClient } from "../../core/factories";
import {
createSilentLogger,
IMPERATIVE_GLOBAL_CONFIG,
expectError,
fixtureFactories,
isRecording,
Expand All @@ -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";
Expand All @@ -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]);
Expand Down Expand Up @@ -235,8 +231,10 @@ async function runFixture(args: string[]): Promise<string> {
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();
Expand Down
Loading
Loading