fix(sdk): preserve nested keys from globalDefaults in configNewProject (#2745)
When building nested config sections (workflow, git, hooks, agent_skills, features), the deep merge was missing globalDefaults for those sections, causing user values from ~/.gsd/defaults.json to be silently dropped. Added globalDefaults spread at the correct precedence level (hardcoded < globalDefaults < userChoices) for all five nested keys, and added three test cases verifying the merge works end-to-end via HOME env var override. Closes #2673 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -245,6 +245,92 @@ describe('configNewProject global defaults (D11)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── configNewProject nested globalDefaults merging ───────────────────────
|
||||
|
||||
describe('configNewProject nested globalDefaults merging (fix #2673)', () => {
|
||||
let fakeHome: string;
|
||||
let originalHome: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeHome = await mkdtemp(join(tmpdir(), 'gsd-fakehome-'));
|
||||
await mkdir(join(fakeHome, '.gsd'), { recursive: true });
|
||||
originalHome = process.env.HOME;
|
||||
process.env.HOME = fakeHome;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalHome !== undefined) {
|
||||
process.env.HOME = originalHome;
|
||||
} else {
|
||||
delete process.env.HOME;
|
||||
}
|
||||
await rm(fakeHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('preserves nested workflow keys from globalDefaults', async () => {
|
||||
await writeFile(
|
||||
join(fakeHome, '.gsd', 'defaults.json'),
|
||||
JSON.stringify({
|
||||
workflow: { auto_advance: true, discuss_mode: 'skip' },
|
||||
git: { branching_strategy: 'milestone' },
|
||||
}),
|
||||
);
|
||||
|
||||
const { configNewProject } = await import('./config-mutation.js');
|
||||
const result = await configNewProject([], tmpDir);
|
||||
expect((result.data as { created: boolean }).created).toBe(true);
|
||||
|
||||
const raw = JSON.parse(await readFile(join(tmpDir, '.planning', 'config.json'), 'utf-8'));
|
||||
// Nested workflow keys from globalDefaults must survive
|
||||
expect(raw.workflow.auto_advance).toBe(true);
|
||||
expect(raw.workflow.discuss_mode).toBe('skip');
|
||||
// Hardcoded defaults not overridden by globalDefaults must still be present
|
||||
expect(raw.workflow.research).toBe(true);
|
||||
// Nested git key from globalDefaults must survive
|
||||
expect(raw.git.branching_strategy).toBe('milestone');
|
||||
// Hardcoded git defaults not overridden must still be present
|
||||
expect(raw.git.phase_branch_template).toBe('gsd/phase-{phase}-{slug}');
|
||||
});
|
||||
|
||||
it('lets userChoices override globalDefaults nested keys', async () => {
|
||||
await writeFile(
|
||||
join(fakeHome, '.gsd', 'defaults.json'),
|
||||
JSON.stringify({
|
||||
workflow: { auto_advance: true },
|
||||
}),
|
||||
);
|
||||
|
||||
const { configNewProject } = await import('./config-mutation.js');
|
||||
const choices = JSON.stringify({ workflow: { auto_advance: false } });
|
||||
const result = await configNewProject([choices], tmpDir);
|
||||
expect((result.data as { created: boolean }).created).toBe(true);
|
||||
|
||||
const raw = JSON.parse(await readFile(join(tmpDir, '.planning', 'config.json'), 'utf-8'));
|
||||
// userChoices must win over globalDefaults
|
||||
expect(raw.workflow.auto_advance).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves nested hooks, agent_skills, and features keys from globalDefaults', async () => {
|
||||
await writeFile(
|
||||
join(fakeHome, '.gsd', 'defaults.json'),
|
||||
JSON.stringify({
|
||||
hooks: { context_warnings: false },
|
||||
agent_skills: { my_skill: true },
|
||||
features: { beta_feature: true },
|
||||
}),
|
||||
);
|
||||
|
||||
const { configNewProject } = await import('./config-mutation.js');
|
||||
const result = await configNewProject([], tmpDir);
|
||||
expect((result.data as { created: boolean }).created).toBe(true);
|
||||
|
||||
const raw = JSON.parse(await readFile(join(tmpDir, '.planning', 'config.json'), 'utf-8'));
|
||||
expect(raw.hooks.context_warnings).toBe(false);
|
||||
expect(raw.agent_skills.my_skill).toBe(true);
|
||||
expect(raw.features.beta_feature).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── configSet ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('configSet', () => {
|
||||
|
||||
@@ -406,22 +406,27 @@ export const configNewProject: QueryHandler = async (args, projectDir, workstrea
|
||||
...userChoices,
|
||||
git: {
|
||||
...(defaults.git as Record<string, unknown>),
|
||||
...((globalDefaults.git as Record<string, unknown>) || {}),
|
||||
...((userChoices.git as Record<string, unknown>) || {}),
|
||||
},
|
||||
workflow: {
|
||||
...(defaults.workflow as Record<string, unknown>),
|
||||
...((globalDefaults.workflow as Record<string, unknown>) || {}),
|
||||
...((userChoices.workflow as Record<string, unknown>) || {}),
|
||||
},
|
||||
hooks: {
|
||||
...(defaults.hooks as Record<string, unknown>),
|
||||
...((globalDefaults.hooks as Record<string, unknown>) || {}),
|
||||
...((userChoices.hooks as Record<string, unknown>) || {}),
|
||||
},
|
||||
agent_skills: {
|
||||
...((defaults.agent_skills as Record<string, unknown>) || {}),
|
||||
...((globalDefaults.agent_skills as Record<string, unknown>) || {}),
|
||||
...((userChoices.agent_skills as Record<string, unknown>) || {}),
|
||||
},
|
||||
features: {
|
||||
...((defaults.features as Record<string, unknown>) || {}),
|
||||
...((globalDefaults.features as Record<string, unknown>) || {}),
|
||||
...((userChoices.features as Record<string, unknown>) || {}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user