Skip to content

Commit f57c301

Browse files
committed
Fix stripping of system task settings and projects
1 parent 035d4f4 commit f57c301

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

src/node/config.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,28 @@ describe("Config", () => {
494494
expect(savedRaw.projects).toEqual([["/user/proj", expect.any(Object)]]);
495495
});
496496

497+
it("system taskSettings and projects are stripped on save", async () => {
498+
writeSystemConfig({
499+
taskSettings: { maxConcurrentTasks: 5 },
500+
projects: [["/sys/project", { workspaces: [] }]],
501+
});
502+
writeUserConfig({
503+
projects: [["/user/project", { workspaces: [] }]],
504+
});
505+
506+
const loaded = config.loadConfigOrDefault();
507+
await config.saveConfig(loaded);
508+
509+
const savedRaw = JSON.parse(
510+
fs.readFileSync(path.join(tempDir, "config.json"), "utf-8")
511+
) as Record<string, unknown>;
512+
// System project should not be in saved user config.
513+
const savedProjects = savedRaw.projects as Array<[string, unknown]>;
514+
const projectPaths = savedProjects.map(([projectPath]) => projectPath);
515+
expect(projectPaths).toContain("/user/project");
516+
expect(projectPaths).not.toContain("/sys/project");
517+
});
518+
497519
it("system object defaults are stripped key-by-key on save", async () => {
498520
writeSystemConfig({
499521
featureFlagOverrides: { flagA: "on", flagB: "off" },

src/node/config.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,15 @@ export class Config {
376376
/**
377377
* Remove fields from save data that match system config defaults,
378378
* preventing materialization of admin-managed baselines into user config.
379-
* Projects and taskSettings are excluded — projects go through normalization
380-
* that changes their shape, and taskSettings always has a default value.
381379
*/
382380
private stripSystemDefaults(
383381
data: Record<string, unknown>,
384382
systemConfig: Partial<AppConfigOnDisk>
385383
): void {
386384
const system = systemConfig as Record<string, unknown>;
387385
for (const key of Object.keys(data)) {
388-
if (key === "projects" || key === "taskSettings") continue;
386+
// Projects handled separately below due to array-of-tuples structure.
387+
if (key === "projects") continue;
389388
const dataVal = data[key];
390389
const sysVal = system[key];
391390
if (sysVal === undefined) continue;
@@ -420,6 +419,33 @@ export class Config {
420419
}
421420
}
422421
}
422+
423+
// Strip system-provided projects that haven't been modified.
424+
// Projects are arrays of [path, config] tuples — compare by path key.
425+
if (Array.isArray(data.projects) && Array.isArray(systemConfig.projects)) {
426+
const systemProjectMap = new Map<string, string>();
427+
for (const entry of systemConfig.projects) {
428+
if (Array.isArray(entry) && entry.length >= 2 && typeof entry[0] === "string") {
429+
const [projectPath, projectConfig] = entry;
430+
systemProjectMap.set(projectPath, JSON.stringify(projectConfig));
431+
}
432+
}
433+
const dataProjects = data.projects as Array<[string, unknown]>;
434+
const filtered = dataProjects.filter(([projectPath, projectConfig]) => {
435+
const systemProjectConfigJson = systemProjectMap.get(projectPath);
436+
if (systemProjectConfigJson === undefined) {
437+
return true; // User-only project.
438+
}
439+
440+
return JSON.stringify(projectConfig) !== systemProjectConfigJson;
441+
});
442+
443+
if (filtered.length === 0) {
444+
delete data.projects;
445+
} else {
446+
data.projects = filtered;
447+
}
448+
}
423449
}
424450

425451
loadConfigOrDefault(): ProjectsConfig {

0 commit comments

Comments
 (0)