diff --git a/src/handlers/project/add/config-bundle/config-bundle.screen.test.tsx b/src/handlers/project/add/config-bundle/config-bundle.screen.test.tsx index 8eeff53af..e70828277 100644 --- a/src/handlers/project/add/config-bundle/config-bundle.screen.test.tsx +++ b/src/handlers/project/add/config-bundle/config-bundle.screen.test.tsx @@ -284,15 +284,14 @@ describe("project add config-bundle wizard", () => { screen.unmount(); }); - test("validates the deployed name against the longest project target", async () => { + test("validates the concatenated project and bundle name", async () => { const projectRoot = await inProject(); await writeFile( join(projectRoot, "agentcore", "aws-targets.json"), JSON.stringify([{ name: "production", account: "111122223333", region: "us-east-1" }]), ); - // 78 characters: legal on its own, one over once the project and the target - // are prefixed. - const name = `c${"x".repeat(77)}`; + + const name = `c${"x".repeat(89)}`; const screen = renderScreen("/agentcore/add/config-bundle"); await waitForText(screen.lastFrame, "what should this configuration bundle be called?"); @@ -301,11 +300,23 @@ describe("project add config-bundle wizard", () => { await waitForFlatText(screen.lastFrame, "is 101 characters. The maximum is 100."); const frame = flatFrame(screen.lastFrame); - expect(frame).toContain("TestProject_production_"); + expect(frame).toContain("Configuration bundle deployed name 'TestProjectc"); expect(frame).toContain("what should this configuration bundle be called?"); screen.unmount(); }); + test("allows a bundle name that fits without the target prefix", async () => { + const projectRoot = await inProject(); + await writeFile( + join(projectRoot, "agentcore", "aws-targets.json"), + JSON.stringify([{ name: "production", account: "111122223333", region: "us-east-1" }]), + ); + const screen = renderScreen("/agentcore/add/config-bundle"); + + await reachComponentsStep(screen, `c${"x".repeat(88)}`); + screen.unmount(); + }); + test("a rejected add reports itself and hands the form back", async () => { const projectRoot = await inProject(); // The name is taken, so addResource refuses it — the realistic failure, and diff --git a/src/handlers/project/add/config-bundle/index.ts b/src/handlers/project/add/config-bundle/index.ts index 71b859d4f..59f700019 100644 --- a/src/handlers/project/add/config-bundle/index.ts +++ b/src/handlers/project/add/config-bundle/index.ts @@ -1,7 +1,6 @@ import z from "zod"; import { InputValidationError } from "../../../../errors"; import { SourceResolver } from "../../../../io"; -import type { AwsDeploymentTarget } from "../../../../projectSchemas/aws-targets"; import { ComponentConfigurationSchema, ConfigBundleBranchNameSchema, @@ -15,7 +14,7 @@ import { formatZodError } from "../../../../router/schema"; import { parseJsonFlag } from "../../../utils"; import type { AddResourceInput, Project } from "../../types"; import type { AddProjectResourceConfig } from "../types"; -import { addProjectResource, requireDeployedNameFits, addDescription } from "../shared"; +import { addProjectResource, addDescription } from "../shared"; // ComponentsSchema is the shape --components accepts. The wizard's components // step validates against it too, rather than a copy of it. @@ -40,15 +39,23 @@ export type ConfigBundleInput = { kmsKeyArn?: string; }; +/** Config bundles keep the v0 physical name without a separator or target. */ +export function requireConfigBundleNameFits(projectName: string, name: string): void { + const deployed = `${projectName}${name}`; + if (deployed.length <= 100) return; + throw new InputValidationError( + `Configuration bundle deployed name '${deployed}' is ${deployed.length} characters. The maximum is 100. The deployed name is . Shorten the configuration bundle name.`, + ); +} + // toAddConfigBundleInput is the one place a configuration bundle is built from // user input, so the flags and the wizard cannot disagree about what a bundle // is, what it defaults to, or how a deployed name is bounded. export function toAddConfigBundleInput( project: Project, - targets: readonly AwsDeploymentTarget[], input: ConfigBundleInput, ): AddResourceInput { - requireDeployedNameFits("Configuration bundle", project.name, input.name, "_", 100, targets); + requireConfigBundleNameFits(project.name, input.name); const components = ComponentsSchema.safeParse(input.components); if (!components.success) { throw new InputValidationError( @@ -112,18 +119,14 @@ export const createAddConfigBundleHandler = (config: AddProjectResourceConfig) = "components", await source.resolveText("components", flags.components), ); - const input = toAddConfigBundleInput( - project, - await config.projectManager.listTargets(project), - { - name: flags.name, - components, - description: flags.description, - branchName: flags["branch-name"], - commitMessage: flags["commit-message"], - kmsKeyArn: flags["kms-key-arn"], - }, - ); + const input = toAddConfigBundleInput(project, { + name: flags.name, + components, + description: flags.description, + branchName: flags["branch-name"], + commitMessage: flags["commit-message"], + kmsKeyArn: flags["kms-key-arn"], + }); await addProjectResource( ctx, diff --git a/src/handlers/project/add/config-bundle/screen.tsx b/src/handlers/project/add/config-bundle/screen.tsx index a75f31f4d..3d95bbb30 100644 --- a/src/handlers/project/add/config-bundle/screen.tsx +++ b/src/handlers/project/add/config-bundle/screen.tsx @@ -9,7 +9,6 @@ import { TextField, Wizard, } from "../../../../components/wizard"; -import type { AwsDeploymentTarget } from "../../../../projectSchemas/aws-targets"; import { ConfigBundleBranchNameSchema, ConfigBundleCommitMessageSchema, @@ -18,11 +17,11 @@ import { import { ProjectKey } from "../../../../router"; import type { ScreenProps } from "../../../types"; import type { Project } from "../../types"; -import { LoadingFrame, ProjectGate, projectQueryKey, useProjectTargets } from "../../ProjectGate"; -import { requireDeployedNameFits } from "../shared"; +import { ProjectGate, projectQueryKey } from "../../ProjectGate"; import { ComponentsSchema, DEFAULT_BRANCH_NAME, + requireConfigBundleNameFits, toAddConfigBundleInput, type ConfigBundleInput, } from "./index"; @@ -97,49 +96,18 @@ export function AddConfigBundleScreen({ ctx, core }: ScreenProps) { onBack={() => navigate(ADD_MENU)} > {(project) => ( - + )} ); } -function AddConfigBundleLoader({ - project, - core, - region, -}: { - project: Project; - core: ScreenProps["core"]; - region: string | undefined; -}) { - const navigate = useNavigate(); - const targets = useProjectTargets(core, project); - - if (targets.data !== undefined) { - return ( - - ); - } - - return ( - navigate(ADD_MENU)} - /> - ); -} - function AddConfigBundleWizard({ project, - targets, core, region, }: { project: Project; - targets: readonly AwsDeploymentTarget[]; core: ScreenProps["core"]; /** The command\'s resolved region, for the China gate of a project without targets. */ region: string | undefined; @@ -155,13 +123,12 @@ function AddConfigBundleWizard({ const set = (update: Partial) => setValues((current) => ({ ...current, ...update })); - // The deployed name is __ and must fit the service - // cap, so the live check reports the real budget rather than the schema's. + // The deployed name is and must fit the service cap. const nameSchema = useMemo( () => ConfigBundleNameSchema.superRefine((name, ctx) => { try { - requireDeployedNameFits("Configuration bundle", project.name, name, "_", 100, targets); + requireConfigBundleNameFits(project.name, name); } catch (error) { ctx.addIssue({ code: "custom", @@ -169,7 +136,7 @@ function AddConfigBundleWizard({ }); } }), - [project.name, targets], + [project.name], ); return ( @@ -180,7 +147,7 @@ function AddConfigBundleWizard({ onSubmit={async function* () { const updated = yield* core.projectManager.addResource( project, - toAddConfigBundleInput(project, targets, toConfigBundleInput(values)), + toAddConfigBundleInput(project, toConfigBundleInput(values)), { region }, ); queryClient.setQueryData(projectQueryKey(), updated); @@ -195,7 +162,7 @@ function AddConfigBundleWizard({ set({ name })} diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 7a064e746..ca670337b 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -1257,6 +1257,17 @@ describe("project add config-bundle", () => { await expect(run(args)).rejects.toBeInstanceOf(InputValidationError); }); + test("accepts a configuration bundle name at the 100-character deployed limit", async () => { + const { projectRoot, cleanup } = await initProject(); + cleanups.push(cleanup); + const name = `c${"x".repeat(88)}`; + + await run(["add", "config-bundle", "--name", name, "--components", JSON.stringify(components)]); + + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.configBundles[0].name).toBe(name); + }); + test.each<[string, string[], string?]>([ [ "missing name", @@ -1271,8 +1282,8 @@ describe("project add config-bundle", () => { ["invalid name", ["--name", "orders-config", "--components", JSON.stringify(components)]], [ "a deployed name over the 100-character service limit", - ["--name", `c${"x".repeat(80)}`, "--components", JSON.stringify(components)], - `Configuration bundle deployed name 'TestProject_default_c${"x".repeat(80)}' is 101 characters. The maximum is 100.`, + ["--name", `c${"x".repeat(89)}`, "--components", JSON.stringify(components)], + `Configuration bundle deployed name 'TestProjectc${"x".repeat(89)}' is 101 characters. The maximum is 100.`, ], ["empty components", ["--name", "OrdersConfig", "--components", "{}"]], [