Skip to content

Commit e263033

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 e263033

5 files changed

Lines changed: 100 additions & 8 deletions

File tree

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

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)