diff --git a/src/handlers/project/add/gateway/index.test.ts b/src/handlers/project/add/gateway/index.test.ts index 802a776df..4bcc3a777 100644 --- a/src/handlers/project/add/gateway/index.test.ts +++ b/src/handlers/project/add/gateway/index.test.ts @@ -1,4 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { expectError } from "../../../../testing"; import { createGatewayProjectTestHarness } from "../gateway-test-support"; import { InputValidationError, ResourceNotFoundError } from "../../../../errors"; @@ -152,6 +154,21 @@ describe("project add gateway", () => { }); }); + test("keeps a hyphenated target as is in the hyphen-separated deployed name", async () => { + const projectRoot = await inProject(); + await writeFile( + join(projectRoot, "agentcore", "aws-targets.json"), + JSON.stringify([{ name: "prod-us1", account: "111122223333", region: "us-east-1" }]), + ); + const name = `g${"x".repeat(80)}`; + + await expectError( + run(["add", "gateway", "--name", name]), + `Gateway deployed name 'TestProject-prod-us1-${name}' is 102 characters. The maximum is 100.`, + InputValidationError, + ); + }); + test.each< [label: string, args: string[], message: string, errorType?: Parameters[2]] >([ diff --git a/src/handlers/project/add/policy-engine/index.test.ts b/src/handlers/project/add/policy-engine/index.test.ts index 2f4682830..18ed25f62 100644 --- a/src/handlers/project/add/policy-engine/index.test.ts +++ b/src/handlers/project/add/policy-engine/index.test.ts @@ -75,6 +75,19 @@ describe("project add policy-engine", () => { ); }); + test("writes a hyphenated target with underscores in the deployed name", async () => { + const projectRoot = await inProject(); + await writeFile( + join(projectRoot, "agentcore", "aws-targets.json"), + JSON.stringify([{ name: "prod-us1", account: "111122223333", region: "us-east-1" }]), + ); + const name = `E${"x".repeat(28)}`; + + await expect(run(["add", "policy-engine", "--name", name])).rejects.toThrow( + `Policy Engine deployed name 'TestProject_prod_us1_${name}' is 50 characters. The maximum is 48.`, + ); + }); + test.each([ ["defaults to enforce", [], "ENFORCE"], ["honors --attach-mode log-only", ["--attach-mode", "log-only"], "LOG_ONLY"], diff --git a/src/handlers/project/add/shared.ts b/src/handlers/project/add/shared.ts index 7be1e0e75..bd03f3416 100644 --- a/src/handlers/project/add/shared.ts +++ b/src/handlers/project/add/shared.ts @@ -37,7 +37,7 @@ type AddProjectResourceResultOptions = { }; /** - The L3 deploys `` and fails at synth past the service cap. This fails at add time using the longest declared target, or "default" when none is declared yet. + The L3 deploys `` and fails at synth past the service cap. This fails at add time using the longest declared target, or "default" when none is declared yet. With the `_` separator the L3 writes the target's hyphens as underscores, which keeps the length. **/ export function requireDeployedNameFits( kind: string, @@ -50,7 +50,8 @@ export function requireDeployedNameFits( const longest = [DEFAULT_TARGET_NAME, ...targets.map((target) => target.name)].reduce((a, b) => b.length > a.length ? b : a, ); - const deployed = [projectName, longest, name].join(separator); + const target = separator === "_" ? longest.replaceAll("-", "_") : longest; + const deployed = [projectName, target, name].join(separator); if (deployed.length <= maxLength) return; throw new InputValidationError( `${kind} deployed name '${deployed}' is ${deployed.length} characters. The maximum is ${maxLength}. The deployed name is ${separator}${separator}, checked with the longest declared target '${longest}'. Shorten the ${kind.toLowerCase()} name.`, diff --git a/src/projectSchemas/aws-targets.test.ts b/src/projectSchemas/aws-targets.test.ts index 2ad154abb..71f9e7747 100644 --- a/src/projectSchemas/aws-targets.test.ts +++ b/src/projectSchemas/aws-targets.test.ts @@ -29,18 +29,19 @@ describe("AWS deployment targets", () => { expect(AwsAccountIdSchema.safeParse(account).success).toBe(false); }); - test.each([ - "", - "1default", - "-default", - "_default", - "has_underscore", - "has-hyphen", - "has spaces", - "has.dots", - ])("rejects invalid target name %j", (name) => { - expect(DeploymentTargetNameSchema.safeParse(name).success).toBe(false); - }); + test.each(["", "1default", "-default", "_default", "has_underscore", "has spaces", "has.dots"])( + "rejects invalid target name %j", + (name) => { + expect(DeploymentTargetNameSchema.safeParse(name).success).toBe(false); + }, + ); + + test.each(["prod-us", "a-b-c", "us-east-1", "trailing-"])( + "accepts hyphenated target name %j, as the v0 CLI did", + (name) => { + expect(DeploymentTargetNameSchema.safeParse(name).success).toBe(true); + }, + ); test("enforces target name and description lengths", () => { expect(DeploymentTargetNameSchema.safeParse("a".repeat(64)).success).toBe(true); diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts index 863e51006..3f9b6146b 100644 --- a/src/projectSchemas/aws-targets.ts +++ b/src/projectSchemas/aws-targets.ts @@ -42,11 +42,11 @@ export const DeploymentTargetNameSchema = z .min(1) .max(64) /** - Letters and digits only: the target is the middle segment of every deployed resource name, so a separator inside it would make two targets compose the same name. + The v0 CLI's rule, so v0 targets such as `prod-us` keep working. The target is the middle segment of deployed resource names (``). Where the separator is `_` because the service disallows `-` (Runtime, Memory, Harness, ...), the CDK library writes the target's hyphens as underscores; `-` and `/` separated names (Gateway, ECR, Lambda, ...) and the `AgentCore--` stack name keep them. **/ .regex( - /^[a-zA-Z][a-zA-Z0-9]*$/, - "Name must start with a letter and contain only letters and digits", + /^[a-zA-Z][a-zA-Z0-9-]*$/, + "Name must start with a letter and contain only letters, digits, and hyphens", ) .describe("Unique identifier for the deployment target");