Skip to content

Commit 2fd73d7

Browse files
committed
fix: inject $schema only on project creation, not on every write
- Move $schema injection from writeProjectSpec to createDefaultProjectSpec in both CLI and TUI create flows - Existing projects are never modified — no git churn on upgrade - Custom $schema values are preserved through read-modify-write cycles - Add integ tests verifying injection, preservation, and non-injection
1 parent 1b9ad51 commit 2fd73d7

5 files changed

Lines changed: 101 additions & 8 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { createTestProject, runCLI } from '../src/test-utils/index.js';
2+
import type { TestProject } from '../src/test-utils/index.js';
3+
import { readFile, writeFile } from 'node:fs/promises';
4+
import { join } from 'node:path';
5+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
6+
7+
const SCHEMA_URL = 'https://schema.agentcore.aws.dev/v1/agentcore.json';
8+
9+
async function readRawConfig(projectPath: string): Promise<Record<string, unknown>> {
10+
const raw = await readFile(join(projectPath, 'agentcore', 'agentcore.json'), 'utf-8');
11+
return JSON.parse(raw) as Record<string, unknown>;
12+
}
13+
14+
describe('integration: $schema injection in agentcore.json', () => {
15+
let project: TestProject;
16+
17+
beforeAll(async () => {
18+
project = await createTestProject({
19+
language: 'Python',
20+
framework: 'Strands',
21+
modelProvider: 'Bedrock',
22+
memory: 'none',
23+
});
24+
});
25+
26+
afterAll(async () => {
27+
await project.cleanup();
28+
});
29+
30+
it('new project has $schema set to the official URL', async () => {
31+
const config = await readRawConfig(project.projectPath);
32+
expect(config.$schema).toBe(SCHEMA_URL);
33+
});
34+
35+
it('$schema appears as the first key in the JSON', async () => {
36+
const config = await readRawConfig(project.projectPath);
37+
expect(Object.keys(config)[0]).toBe('$schema');
38+
});
39+
40+
it('$schema persists after adding a resource', async () => {
41+
const memName = `SchemaMem${Date.now().toString().slice(-6)}`;
42+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
43+
44+
const config = await readRawConfig(project.projectPath);
45+
expect(config.$schema).toBe(SCHEMA_URL);
46+
47+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
48+
});
49+
50+
it('does not overwrite a custom $schema value', async () => {
51+
const configPath = join(project.projectPath, 'agentcore', 'agentcore.json');
52+
const config = await readRawConfig(project.projectPath);
53+
const customUrl = 'https://example.com/custom-schema.json';
54+
config.$schema = customUrl;
55+
await writeFile(configPath, JSON.stringify(config, null, 2));
56+
57+
const memName = `CustomMem${Date.now().toString().slice(-6)}`;
58+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
59+
60+
const updated = await readRawConfig(project.projectPath);
61+
expect(updated.$schema).toBe(customUrl);
62+
63+
// Restore original
64+
updated.$schema = SCHEMA_URL;
65+
await writeFile(configPath, JSON.stringify(updated, null, 2));
66+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
67+
});
68+
69+
it('does not inject $schema into a pre-existing project that lacks one', async () => {
70+
const configPath = join(project.projectPath, 'agentcore', 'agentcore.json');
71+
const config = await readRawConfig(project.projectPath);
72+
73+
// Simulate an old project by stripping $schema
74+
delete config.$schema;
75+
await writeFile(configPath, JSON.stringify(config, null, 2));
76+
77+
// Trigger a write
78+
const memName = `OldProj${Date.now().toString().slice(-6)}`;
79+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
80+
81+
const updated = await readRawConfig(project.projectPath);
82+
expect(updated.$schema).toBeUndefined();
83+
84+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
85+
});
86+
});

src/cli/commands/create/action.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { APP_DIR, CONFIG_DIR, ConfigIO, setEnvVar, setSessionProjectRoot } from '../../../lib';
1+
import { APP_DIR, CONFIG_DIR, ConfigIO, SCHEMA_URL, setEnvVar, setSessionProjectRoot } from '../../../lib';
22
import type {
33
AgentCoreProjectSpec,
44
BuildType,
@@ -26,6 +26,7 @@ import { join } from 'path';
2626

2727
function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
2828
return {
29+
$schema: SCHEMA_URL,
2930
name: projectName,
3031
version: 1,
3132
agents: [],

src/cli/tui/screens/create/useCreateFlow.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { APP_DIR, CONFIG_DIR, ConfigIO, findConfigRoot, setEnvVar, setSessionProjectRoot } from '../../../../lib';
1+
import {
2+
APP_DIR,
3+
CONFIG_DIR,
4+
ConfigIO,
5+
SCHEMA_URL,
6+
findConfigRoot,
7+
setEnvVar,
8+
setSessionProjectRoot,
9+
} from '../../../../lib';
210
import type { AgentCoreProjectSpec, DeployedState } from '../../../../schema';
311
import { getErrorMessage } from '../../../errors';
412
import { CreateLogger } from '../../../logging';
@@ -71,6 +79,7 @@ function getCreateSteps(projectName: string, agentConfig: AddAgentConfig | null)
7179

7280
function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
7381
return {
82+
$schema: SCHEMA_URL,
7483
name: projectName,
7584
version: 1,
7685
tags: {

src/lib/schemas/io/config-io.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { mkdir, readFile, writeFile } from 'fs/promises';
2121
import { dirname } from 'path';
2222
import { type ZodType } from 'zod';
2323

24-
const SCHEMA_URL = 'https://schema.agentcore.aws.dev/v1/agentcore.json';
24+
export const SCHEMA_URL = 'https://schema.agentcore.aws.dev/v1/agentcore.json';
2525

2626
/**
2727
* Manages reading, writing, and validation of AgentCore configuration files
@@ -105,10 +105,7 @@ export class ConfigIO {
105105
*/
106106
async writeProjectSpec(data: AgentCoreProjectSpec): Promise<void> {
107107
const filePath = this.pathResolver.getAgentConfigPath();
108-
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, {
109-
...data,
110-
$schema: SCHEMA_URL,
111-
});
108+
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, data);
112109
}
113110

114111
/**

src/lib/schemas/io/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ export {
1010
NoProjectError,
1111
type PathConfig,
1212
} from './path-resolver';
13-
export { ConfigIO, createConfigIO } from './config-io';
13+
export { ConfigIO, createConfigIO, SCHEMA_URL } from './config-io';
1414
export { readCliConfig, type CliConfig } from './cli-config';

0 commit comments

Comments
 (0)