Skip to content
Draft
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
119 changes: 118 additions & 1 deletion src/cli/commands/create/__tests__/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ describe('create command', () => {
expect(json.success).toBe(false);
expect(json.error.includes('conflicts')).toBeTruthy();
});

it('creates project-only scaffold with --project-name and no --name', async () => {
const projectName = `ProjOnly${Date.now()}`;
const result = await runCLI(['create', '--project-name', projectName, '--no-agent', '--json'], testDir);

expect(result.exitCode, `stderr: ${result.stderr}, stdout: ${result.stdout}`).toBe(0);

const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
expect(await exists(join(json.projectPath, 'agentcore'))).toBeTruthy();
});
});

describe('with agent', () => {
Expand Down Expand Up @@ -145,6 +157,82 @@ describe('create command', () => {
expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']);
expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']);
});

it('uses --project-name for project and --name for agent resource', async () => {
const projectName = `AgentProj${Date.now().toString().slice(-6)}`;
const agentName = `AgentResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
const result = await runCLI(
[
'create',
'--project-name',
projectName,
'--name',
agentName,
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--skip-git',
'--skip-install',
'--json',
],
testDir
);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
expect(json.agentName).toBe(agentName);
expect(await exists(join(json.projectPath, 'app', agentName))).toBeTruthy();

const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
expect(projectSpec.name).toBe(projectName);
expect(projectSpec.runtimes[0].name).toBe(agentName);
});
});

describe('with harness', () => {
it('uses --project-name for project and --name for harness resource', async () => {
const projectName = `HarnessProj${Date.now().toString().slice(-6)}`;
const harnessName = `HarnessResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
const result = await runCLI(
['create', '--project-name', projectName, '--name', harnessName, '--skip-git', '--skip-install', '--json'],
testDir
);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
expect(await exists(join(json.projectPath, 'app', harnessName, 'harness.json'))).toBeTruthy();

const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
expect(projectSpec.name).toBe(projectName);
expect(projectSpec.harnesses[0].name).toBe(harnessName);
expect(projectSpec.harnesses[0].path).toBe(`app/${harnessName}`);
});

it('rejects long harness name without --project-name but accepts it with --project-name', async () => {
const harnessName = `Harness${'A'.repeat(30)}`;
const rejected = await runCLI(['create', '--name', harnessName, '--skip-install', '--json'], testDir);
expect(rejected.exitCode).toBe(1);
expect(JSON.parse(rejected.stdout).success).toBe(false);

const projectName = `ShortProj${Date.now().toString().slice(-6)}`;
const accepted = await runCLI(
['create', '--project-name', projectName, '--name', harnessName, '--skip-git', '--skip-install', '--json'],
testDir
);
expect(accepted.exitCode, `stdout: ${accepted.stdout}, stderr: ${accepted.stderr}`).toBe(0);
expect(JSON.parse(accepted.stdout).success).toBe(true);
});
});

describe('--defaults', () => {
Expand All @@ -163,12 +251,41 @@ describe('create command', () => {
it('shows files without creating', async () => {
const name = `DryRun${Date.now()}`;
// --framework triggers agent path where --dry-run is supported
const result = await runCLI(['create', '--name', name, '--defaults', '--framework', 'Strands', '--dry-run'], testDir);
const result = await runCLI(
['create', '--name', name, '--defaults', '--framework', 'Strands', '--dry-run'],
testDir
);

expect(result.exitCode).toBe(0);
expect(result.stdout.includes('would create') || result.stdout.includes('Dry run')).toBeTruthy();
expect(await exists(join(testDir, name)), 'Should not create directory').toBe(false);
});

it('uses project-name for project paths and name for app paths', async () => {
const projectName = `DryProj${Date.now().toString().slice(-6)}`;
const agentName = `DryAgent${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'create',
'--project-name',
projectName,
'--name',
agentName,
'--defaults',
'--framework',
'Strands',
'--dry-run',
'--json',
],
testDir
);

expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
expect(json.wouldCreate).toContain(`${json.projectPath}/app/${agentName}/`);
expect(await exists(join(testDir, projectName)), 'Should not create directory').toBe(false);
});
});

describe('--skip-git', () => {
Expand Down
20 changes: 20 additions & 0 deletions src/cli/commands/create/__tests__/harness-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ describe('createProjectWithHarness', () => {
await expect(exists(join(harnessDir, 'system-prompt.md'))).resolves.toBe(true);
});

it('uses projectName for project scaffold and name for harness resource', async () => {
const projectName = `Proj${randomUUID().slice(0, 6)}`;
const name = `HarnessName${randomUUID().replace(/-/g, '').slice(0, 12)}`;
const result = await createProjectWithHarness({
name,
projectName,
cwd: testDir,
modelProvider: 'bedrock',
modelId: 'global.anthropic.claude-sonnet-4-6',
skipGit: true,
skipInstall: true,
});

expect(result.success, `Error: ${result.error}`).toBe(true);
expect(result.projectPath).toBe(join(testDir, projectName));

await expect(exists(join(result.projectPath!, 'agentcore'))).resolves.toBe(true);
await expect(exists(join(result.projectPath!, 'app', name, 'harness.json'))).resolves.toBe(true);
});

it('creates harness with custom options', async () => {
const name = `CustomH${randomUUID().slice(0, 6)}`;
const result = await createProjectWithHarness({
Expand Down
31 changes: 29 additions & 2 deletions src/cli/commands/create/__tests__/harness-validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('validateCreateHarnessOptions', () => {
testDir = join(tmpdir(), `harness-create-validate-${randomUUID()}`);
mkdirSync(testDir, { recursive: true });
mkdirSync(join(testDir, 'existingHarness'), { recursive: true });
mkdirSync(join(testDir, 'existingProject'), { recursive: true });
});

afterAll(() => {
Expand Down Expand Up @@ -140,13 +141,39 @@ describe('validateCreateHarnessOptions', () => {
expect(options.modelId).toBe('global.anthropic.claude-sonnet-4-6');
});

it('accepts valid harness name with underscores', () => {
const result = validateCreateHarnessOptions({ name: 'my_valid_harness_123' }, testDir);
it('accepts valid harness name with underscores when project-name is valid', () => {
const result = validateCreateHarnessOptions(
{ name: 'my_valid_harness_123', projectName: 'myValidHarness123' },
testDir
);
expect(result.valid).toBe(true);
});

it('rejects harness name longer than 48 characters', () => {
const result = validateCreateHarnessOptions({ name: 'a'.repeat(49) }, testDir);
expect(result.valid).toBe(false);
});

it('allows long harness name when project-name is valid', () => {
const result = validateCreateHarnessOptions(
{ name: `Harness${'A'.repeat(30)}`, projectName: 'ShortProject' },
testDir
);
expect(result.valid).toBe(true);
});

it('validates project-name separately from harness name', () => {
const result = validateCreateHarnessOptions(
{ name: 'ValidHarness', projectName: 'ProjectNameTooLongForCli' },
testDir
);
expect(result.valid).toBe(false);
expect(result.error).toContain('Project name');
});

it('checks folder existence using project-name', () => {
const result = validateCreateHarnessOptions({ name: 'ValidHarness2', projectName: 'existingProject' }, testDir);
expect(result.valid).toBe(false);
expect(result.error).toContain('existingProject');
});
});
37 changes: 37 additions & 0 deletions src/cli/commands/create/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('validateCreateOptions', () => {
beforeAll(() => {
testDir = join(tmpdir(), `create-opts-${randomUUID()}`);
mkdirSync(testDir, { recursive: true });
mkdirSync(join(testDir, 'ExistingProject'), { recursive: true });
});

afterAll(() => {
Expand All @@ -59,6 +60,42 @@ describe('validateCreateOptions', () => {
expect(result.error).toContain('already exists');
});

it('validates projectName separately from agent name', () => {
const result = validateCreateOptions(
{
name: `Agent${'A'.repeat(30)}`,
projectName: 'ShortProject',
language: 'Python',
framework: 'Strands',
modelProvider: 'Bedrock',
memory: 'none',
},
testDir
);
expect(result.valid).toBe(true);
});

it('checks folder existence using projectName', () => {
const result = validateCreateOptions(
{
name: 'AgentName',
projectName: 'ExistingProject',
language: 'Python',
framework: 'Strands',
modelProvider: 'Bedrock',
memory: 'none',
},
testDir
);
expect(result.valid).toBe(false);
expect(result.error).toContain('ExistingProject');
});

it('allows project-only create with only projectName', () => {
const result = validateCreateOptions({ projectName: 'OnlyProject', agent: false }, testDir);
expect(result.valid).toBe(true);
});

it('returns valid with --no-agent flag', () => {
const result = validateCreateOptions({ name: 'NoAgentProject', agent: false }, testDir);
expect(result.valid).toBe(true);
Expand Down
26 changes: 21 additions & 5 deletions src/cli/commands/create/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm';

export interface CreateWithAgentOptions {
name: string;
projectName?: string;
cwd: string;
type?: 'create' | 'import';
buildType?: BuildType;
Expand Down Expand Up @@ -139,6 +140,7 @@ export interface CreateWithAgentOptions {
export async function createProjectWithAgent(options: CreateWithAgentOptions): Promise<CreateResult> {
const {
name,
projectName: explicitProjectName,
cwd,
buildType,
language,
Expand All @@ -159,7 +161,8 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
skipPythonSetup,
onProgress,
} = options;
const projectRoot = join(cwd, name);
const projectName = explicitProjectName ?? name;
const projectRoot = join(cwd, projectName);
const configBaseDir = join(projectRoot, CONFIG_DIR);

// Check CLI dependencies first (with language for conditional uv check)
Expand All @@ -172,7 +175,14 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
}

// First create the base project (skip dependency check since we already did it)
const projectResult = await createProject({ name, cwd, skipGit, skipInstall, skipDependencyCheck: true, onProgress });
const projectResult = await createProject({
name: projectName,
cwd,
skipGit,
skipInstall,
skipDependencyCheck: true,
onProgress,
});
if (!projectResult.success) {
// Merge warnings from both checks
const allWarnings = [...depWarnings, ...(projectResult.warnings ?? [])];
Expand Down Expand Up @@ -243,7 +253,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P

if (!isMcp && resolvedModelProvider !== 'Bedrock') {
strategy = await credentialPrimitive.resolveCredentialStrategy(
name,
projectName,
agentName,
resolvedModelProvider,
apiKey,
Expand Down Expand Up @@ -295,9 +305,15 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
}
}

export function getDryRunInfo(options: { name: string; cwd: string; language?: string }): CreateResult {
export function getDryRunInfo(options: {
name: string;
cwd: string;
language?: string;
projectName?: string;
}): CreateResult {
const { name, cwd, language } = options;
const projectRoot = join(cwd, name);
const projectName = options.projectName ?? name;
const projectRoot = join(cwd, projectName);

const wouldCreate = [
`${projectRoot}/`,
Expand Down
Loading
Loading