From 9e5471f02117411b3b54a34f832ebaac0cc250a2 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Mon, 12 Oct 2026 19:05:03 +0000 Subject: [PATCH] feat(compat): fail when project version and CDK library disagree --- src/core/project/manager.test.ts | 123 +++++++++++++++++++++++++++++-- src/core/project/manager.tsx | 58 ++++++++++++--- 2 files changed, 164 insertions(+), 17 deletions(-) diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 9762e251b..2c9401244 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -2110,7 +2110,6 @@ describe("FsProjectManager.resolve", () => { "0.1.0-alpha.45", "0.1.0-alpha.46", "0.1.0", - "1.0.0-rc.3", "^0.1.0-alpha.45", undefined, 19, @@ -2133,7 +2132,7 @@ describe("FsProjectManager.resolve", () => { }); }); - test.each([undefined, 0, 2, 3, "1"])( + test.each([undefined, 0, 3, "1"])( "does not apply v0 dependency guidance to project version %s", async (version) => { const { root } = await legacyProject("^0.1.0-alpha.19", version); @@ -2260,16 +2259,126 @@ describe("FsProjectManager.resolve", () => { ).rejects.toThrow("'export harness' isn't available for v0 projects yet"); }); - test("loads a v1 project with an old CDK declaration without legacy guidance", async () => { - const { root, configPath } = await legacyProject("^0.1.0-alpha.19", 2); + async function v1Project(dependency: unknown): Promise<{ root: string; configPath: string }> { + const { root, configPath } = await legacyProject(dependency, 2); await writeFile(configPath, JSON.stringify({ name: "example", version: 2 })); + return { root, configPath }; + } - const project = await manager().manager.resolve({ filePath: root }); + function v1WithV0LibraryMessage(spec: string): string { + return ( + "agentcore/agentcore.json says this is a v1 project (version 2), but its CDK library is " + + `v0 (@aws/agentcore-cdk ${spec}). Set "version" back to 1, or run \`agentcore migrate\` ` + + "to upgrade it." + ); + } + + function v0WithV1LibraryMessage(spec: string): string { + return ( + "agentcore/agentcore.json says this is a v0 project (version 1), but its CDK library is " + + `v1 (@aws/agentcore-cdk ${spec}). Set "version" to 2, or reinstall the v0 CDK app.` + ); + } + + test.each([ + ["^0.1.0-alpha.19", "^0.1.0-alpha.19"], + ["0.1.0-alpha.45", "0.1.0-alpha.45"], + ["~0.1.0-alpha.45", "~0.1.0-alpha.45"], + ["0.1.0", "0.1.0"], + [" ^0.1.0-alpha.45 ", "^0.1.0-alpha.45"], + ])( + "rejects a v1 project whose CDK library is v0 (%s) without changing files", + async (dependency, shown) => { + const { root, configPath } = await v1Project(dependency); + const manifestPath = join(root, "agentcore", "cdk", "package.json"); + const before = await Promise.all([ + readFile(configPath, "utf8"), + readFile(manifestPath, "utf8"), + ]); + const subject = manager(); + + const error = await subject.manager + .resolve({ filePath: root }) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(ProjectStateError); + expect(error).toHaveProperty("message", v1WithV0LibraryMessage(shown)); + expect( + await Promise.all([readFile(configPath, "utf8"), readFile(manifestPath, "utf8")]), + ).toEqual(before); + expect(subject.commands).toEqual([]); + expect(subject.checkedTools).toEqual([]); + }, + ); + + test("rejects a v1 project with a v0 CDK library even when its spec is invalid", async () => { + const { root } = await legacyProject("^0.1.0-alpha.19", 2); + + const error = await manager() + .manager.resolve({ filePath: root }) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(ProjectStateError); + expect(error).toHaveProperty("message", v1WithV0LibraryMessage("^0.1.0-alpha.19")); + expect((error as Error).cause).toBeInstanceOf(DeserializationError); + }); + + test.each(["1.0.0-rc.5", "^1.0.0-rc.5", "~1.0.0-preview.33", "1.0.0", "2.1.0"])( + "rejects a v0 project whose CDK library is v1 (%s)", + async (dependency) => { + const { root } = await legacyProject(dependency, 1); + const subject = manager(); + + const error = await subject.manager + .resolve({ filePath: root }) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(ProjectStateError); + expect(error).toHaveProperty("message", v0WithV1LibraryMessage(dependency)); + expect(subject.commands).toEqual([]); + }, + ); - expect(project?.spec.version).toBe(2); - expect(project?.name).toBe("example"); + test.each(["1.0.0-rc.5", "^1.0.0-preview.33", "~1.0.0", "1.2.3"])( + "loads a v1 project with v1 CDK library %s", + async (dependency) => { + const { root } = await v1Project(dependency); + + const project = await manager().manager.resolve({ filePath: root }); + + expect(project?.spec.version).toBe(2); + expect(project?.legacy).toBeUndefined(); + }, + ); + + test.each([ + undefined, + 19, + "not-semver", + "file:../agentcore-cdk", + "git+https://example.com/agentcore-cdk.git", + "workspace:*", + "latest", + "*", + ">=0.1.0-alpha.19", + ])("loads a v1 project with unrecognized CDK dependency %s", async (dependency) => { + const { root } = await v1Project(dependency); + + expect((await manager().manager.resolve({ filePath: root }))?.spec.version).toBe(2); }); + test.each(["missing", "malformed", "invalid-shape"] as const)( + "loads a v1 project when its CDK manifest is %s", + async (state) => { + const { root } = await v1Project("^0.1.0-alpha.19"); + const manifestPath = join(root, "agentcore", "cdk", "package.json"); + if (state === "missing") await rm(manifestPath); + else await writeFile(manifestPath, state === "malformed" ? "{ not valid json" : "null"); + + expect((await manager().manager.resolve({ filePath: root }))?.spec.version).toBe(2); + }, + ); + test("round-trips a project it just created", async () => { const root = await inTempDirectory(); const subject = manager().manager; diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index b8f2c251c..0254b003a 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -106,7 +106,7 @@ import type { CoreIdentityClient } from "../../handlers/identity/types"; import { templateManagesDependencies } from "../../handlers/project/templateProfile"; import { resolveRuntimeTemplateProfile } from "../../handlers/project/runtimeTemplateProfile"; import { isBmaRuntime } from "../../handlers/project/bma"; -import { projectFormat } from "./legacy/detect"; +import { projectFormat, type ProjectFormat } from "./legacy/detect"; import { loadLegacyProject } from "./legacy/load"; import { assertV1Project } from "./legacy/writeGate"; @@ -117,7 +117,43 @@ const UV_INSTALL_HINT = "Install uv: https://docs.astral.sh/uv/getting-started/i // v0.24.2 introduced dependency syncing with this exact CDK pin. const MINIMUM_V0_CDK_VERSION = "0.1.0-alpha.45"; +interface CdkDependency { + /** As written in package.json, trimmed. */ + spec: string; + version: string; +} + +function v0DependencyWarning(dependency: CdkDependency | undefined): string | undefined { + if (!dependency || !semver.lt(dependency.version, MINIMUM_V0_CDK_VERSION)) return undefined; + return ( + "This project's CDK dependencies are older than v0.24.2. " + + "Run `npx @aws/agentcore@0 deploy` once to update them, then try again." + ); +} + +/** Rejects a project whose agentcore.json format and vended CDK library major disagree. */ +function assertCdkLibraryMatches( + format: ProjectFormat, + dependency: CdkDependency | undefined, + cause?: unknown, +): void { + if (!dependency) return; + const library: ProjectFormat = semver.major(dependency.version) >= 1 ? "v1" : "v0"; + if (library === format) return; + + const declared = `@aws/agentcore-cdk ${dependency.spec}`; + throw new ProjectStateError( + format === "v1" + ? "agentcore/agentcore.json says this is a v1 project (version 2), but its CDK library is " + + `v0 (${declared}). Set "version" back to 1, or run \`agentcore migrate\` to upgrade it.` + : "agentcore/agentcore.json says this is a v0 project (version 1), but its CDK library is " + + `v1 (${declared}). Set "version" to 2, or reinstall the v0 CDK app.`, + { cause }, + ); +} + /** + * Shown when a runtime template hardwired to an unreachable model provider * targets a China (aws-cn) region and takes no --model-provider override * (langchain, vercel, `--type import`). Amazon Bedrock, Anthropic, OpenAI, and @@ -402,8 +438,10 @@ export class FsProjectManager implements ProjectManager { if (!rootPath) return undefined; const configPath = projectSpecPath(rootPath); + const cdkDependency = await this.declaredCdkDependency(rootPath); try { const spec = await this.json.read(configPath, ProjectSpecSchema); + assertCdkLibraryMatches("v1", cdkDependency); return { name: spec.name, rootPath, @@ -415,16 +453,20 @@ export class FsProjectManager implements ProjectManager { .read(configPath, z.object({ version: z.unknown().optional() })) .catch(() => undefined); if (projectFormat(header) === "v0") { - const warning = await this.v0DependencyWarning(rootPath); + const warning = v0DependencyWarning(cdkDependency); if (warning) throw new ProjectStateError(warning, { cause: error }); + assertCdkLibraryMatches("v0", cdkDependency, error); return loadLegacyProject(rootPath, this.json); } + // An invalid v1 spec on a v0 CDK app is almost always a hand-bumped version. + if (header?.version === 2) assertCdkLibraryMatches("v1", cdkDependency, error); } throw error; } } - private async v0DependencyWarning(rootPath: string): Promise { + /** The `@aws/agentcore-cdk` dependency the vended CDK app declares, if it is a plain semver. */ + private async declaredCdkDependency(rootPath: string): Promise { const manifest = await this.json .read( join(rootPath, "agentcore", "cdk", "package.json"), @@ -434,13 +476,9 @@ export class FsProjectManager implements ProjectManager { const declared = manifest?.dependencies?.["@aws/agentcore-cdk"]; if (typeof declared !== "string") return undefined; - const version = semver.valid(declared.trim().replace(/^[~^=]/, "")); - if (!version || !semver.lt(version, MINIMUM_V0_CDK_VERSION)) return undefined; - - return ( - "This project's CDK dependencies are older than v0.24.2. " + - "Run `npx @aws/agentcore@0 deploy` once to update them, then try again." - ); + const spec = declared.trim(); + const version = semver.valid(spec.replace(/^[~^=]/, "")); + return version ? { spec, version } : undefined; } public async *create(input: CreateProjectInput): AsyncGenerator {