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
123 changes: 116 additions & 7 deletions src/core/project/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
58 changes: 48 additions & 10 deletions src/core/project/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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<string | undefined> {
/** The `@aws/agentcore-cdk` dependency the vended CDK app declares, if it is a plain semver. */
private async declaredCdkDependency(rootPath: string): Promise<CdkDependency | undefined> {
const manifest = await this.json
.read(
join(rootPath, "agentcore", "cdk", "package.json"),
Expand All @@ -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<ProjectEvent, Project> {
Expand Down
Loading