Skip to content

Commit d91f315

Browse files
authored
Merge pull request #87 from ShchedrinAndrei/feature/preserve-project-runtime-defaults
fix: preserve project runtime defaults on update
2 parents 8c23f1c + 59e377c commit d91f315

3 files changed

Lines changed: 141 additions & 18 deletions

File tree

packages/api/src/__tests__/projects.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,92 @@ describe("projects API", () => {
393393
expect(refetched.defaultReviewRuntimeProfileId).toBe("profile-review");
394394
});
395395

396+
it("preserves project runtime defaults when update payload omits them", async () => {
397+
testDb.current
398+
.insert(runtimeProfiles)
399+
.values([
400+
{
401+
id: "profile-task",
402+
projectId: null,
403+
name: "Task Profile",
404+
runtimeId: "claude",
405+
providerId: "anthropic",
406+
enabled: true,
407+
},
408+
{
409+
id: "profile-plan",
410+
projectId: null,
411+
name: "Plan Profile",
412+
runtimeId: "claude",
413+
providerId: "anthropic",
414+
enabled: true,
415+
},
416+
{
417+
id: "profile-review",
418+
projectId: null,
419+
name: "Review Profile",
420+
runtimeId: "claude",
421+
providerId: "anthropic",
422+
enabled: true,
423+
},
424+
{
425+
id: "profile-chat",
426+
projectId: null,
427+
name: "Chat Profile",
428+
runtimeId: "claude",
429+
providerId: "anthropic",
430+
enabled: true,
431+
},
432+
])
433+
.run();
434+
435+
const createRes = await app.request("/projects", {
436+
method: "POST",
437+
headers: { "Content-Type": "application/json" },
438+
body: JSON.stringify({
439+
name: "Runtime Defaults",
440+
rootPath: "/tmp/runtime-defaults-project",
441+
defaultTaskRuntimeProfileId: "profile-task",
442+
defaultPlanRuntimeProfileId: "profile-plan",
443+
defaultReviewRuntimeProfileId: "profile-review",
444+
defaultChatRuntimeProfileId: "profile-chat",
445+
}),
446+
});
447+
expect(createRes.status).toBe(201);
448+
const created = await createRes.json();
449+
450+
const updateRes = await app.request(`/projects/${created.id}`, {
451+
method: "PUT",
452+
headers: { "Content-Type": "application/json" },
453+
body: JSON.stringify({
454+
name: "Runtime Defaults Renamed",
455+
rootPath: "/tmp/runtime-defaults-project-renamed",
456+
}),
457+
});
458+
expect(updateRes.status).toBe(200);
459+
const updated = await updateRes.json();
460+
expect(updated.defaultTaskRuntimeProfileId).toBe("profile-task");
461+
expect(updated.defaultPlanRuntimeProfileId).toBe("profile-plan");
462+
expect(updated.defaultReviewRuntimeProfileId).toBe("profile-review");
463+
expect(updated.defaultChatRuntimeProfileId).toBe("profile-chat");
464+
465+
const clearRes = await app.request(`/projects/${created.id}`, {
466+
method: "PUT",
467+
headers: { "Content-Type": "application/json" },
468+
body: JSON.stringify({
469+
name: "Runtime Defaults Renamed",
470+
rootPath: "/tmp/runtime-defaults-project-renamed",
471+
defaultPlanRuntimeProfileId: null,
472+
}),
473+
});
474+
expect(clearRes.status).toBe(200);
475+
const cleared = await clearRes.json();
476+
expect(cleared.defaultTaskRuntimeProfileId).toBe("profile-task");
477+
expect(cleared.defaultPlanRuntimeProfileId).toBeNull();
478+
expect(cleared.defaultReviewRuntimeProfileId).toBe("profile-review");
479+
expect(cleared.defaultChatRuntimeProfileId).toBe("profile-chat");
480+
});
481+
396482
it("rejects foreign project-owned runtime profile defaults on update", async () => {
397483
testDb.current
398484
.insert(runtimeProfiles)

packages/data/src/__tests__/index.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,33 @@ describe("data layer", () => {
384384
expect(updated!.rootPath).toBe("/tmp/updated");
385385
});
386386

387+
it("updateProject preserves omitted runtime defaults and clears explicit nulls", () => {
388+
const p = createProject({
389+
name: "P",
390+
rootPath: "/tmp/p",
391+
defaultTaskRuntimeProfileId: "task-profile",
392+
defaultPlanRuntimeProfileId: "plan-profile",
393+
defaultReviewRuntimeProfileId: "review-profile",
394+
defaultChatRuntimeProfileId: "chat-profile",
395+
});
396+
397+
const renamed = updateProject(p!.id, { name: "Renamed", rootPath: "/tmp/renamed" });
398+
expect(renamed!.defaultTaskRuntimeProfileId).toBe("task-profile");
399+
expect(renamed!.defaultPlanRuntimeProfileId).toBe("plan-profile");
400+
expect(renamed!.defaultReviewRuntimeProfileId).toBe("review-profile");
401+
expect(renamed!.defaultChatRuntimeProfileId).toBe("chat-profile");
402+
403+
const cleared = updateProject(p!.id, {
404+
name: "Renamed",
405+
rootPath: "/tmp/renamed",
406+
defaultPlanRuntimeProfileId: null,
407+
});
408+
expect(cleared!.defaultTaskRuntimeProfileId).toBe("task-profile");
409+
expect(cleared!.defaultPlanRuntimeProfileId).toBeNull();
410+
expect(cleared!.defaultReviewRuntimeProfileId).toBe("review-profile");
411+
expect(cleared!.defaultChatRuntimeProfileId).toBe("chat-profile");
412+
});
413+
387414
it("deleteProject removes project", () => {
388415
const p = createProject({ name: "Del", rootPath: "/tmp/del" });
389416
deleteProject(p!.id);

packages/data/src/index.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,32 +1098,42 @@ export function updateProject(
10981098
defaultChatRuntimeProfileId?: string | null;
10991099
},
11001100
): ProjectRow | undefined {
1101+
const patch: Partial<ProjectRow> = {
1102+
name: input.name,
1103+
rootPath: input.rootPath,
1104+
plannerMaxBudgetUsd: input.plannerMaxBudgetUsd ?? null,
1105+
planCheckerMaxBudgetUsd: input.planCheckerMaxBudgetUsd ?? null,
1106+
implementerMaxBudgetUsd: input.implementerMaxBudgetUsd ?? null,
1107+
reviewSidecarMaxBudgetUsd: input.reviewSidecarMaxBudgetUsd ?? null,
1108+
parallelEnabled: input.parallelEnabled ?? false,
1109+
updatedAt: new Date().toISOString(),
1110+
};
1111+
if (input.defaultTaskRuntimeProfileId !== undefined) {
1112+
patch.defaultTaskRuntimeProfileId = input.defaultTaskRuntimeProfileId;
1113+
}
1114+
if (input.defaultPlanRuntimeProfileId !== undefined) {
1115+
patch.defaultPlanRuntimeProfileId = input.defaultPlanRuntimeProfileId;
1116+
}
1117+
if (input.defaultReviewRuntimeProfileId !== undefined) {
1118+
patch.defaultReviewRuntimeProfileId = input.defaultReviewRuntimeProfileId;
1119+
}
1120+
if (input.defaultChatRuntimeProfileId !== undefined) {
1121+
patch.defaultChatRuntimeProfileId = input.defaultChatRuntimeProfileId;
1122+
}
1123+
11011124
log.debug(
11021125
{
11031126
projectId: id,
1104-
defaultTaskRuntimeProfileId: input.defaultTaskRuntimeProfileId ?? null,
1105-
defaultPlanRuntimeProfileId: input.defaultPlanRuntimeProfileId ?? null,
1106-
defaultReviewRuntimeProfileId: input.defaultReviewRuntimeProfileId ?? null,
1107-
defaultChatRuntimeProfileId: input.defaultChatRuntimeProfileId ?? null,
1127+
defaultTaskRuntimeProfileId: patch.defaultTaskRuntimeProfileId ?? null,
1128+
defaultPlanRuntimeProfileId: patch.defaultPlanRuntimeProfileId ?? null,
1129+
defaultReviewRuntimeProfileId: patch.defaultReviewRuntimeProfileId ?? null,
1130+
defaultChatRuntimeProfileId: patch.defaultChatRuntimeProfileId ?? null,
11081131
},
11091132
"Updating project runtime defaults",
11101133
);
11111134
getDb()
11121135
.update(projects)
1113-
.set({
1114-
name: input.name,
1115-
rootPath: input.rootPath,
1116-
plannerMaxBudgetUsd: input.plannerMaxBudgetUsd ?? null,
1117-
planCheckerMaxBudgetUsd: input.planCheckerMaxBudgetUsd ?? null,
1118-
implementerMaxBudgetUsd: input.implementerMaxBudgetUsd ?? null,
1119-
reviewSidecarMaxBudgetUsd: input.reviewSidecarMaxBudgetUsd ?? null,
1120-
parallelEnabled: input.parallelEnabled ?? false,
1121-
defaultTaskRuntimeProfileId: input.defaultTaskRuntimeProfileId ?? null,
1122-
defaultPlanRuntimeProfileId: input.defaultPlanRuntimeProfileId ?? null,
1123-
defaultReviewRuntimeProfileId: input.defaultReviewRuntimeProfileId ?? null,
1124-
defaultChatRuntimeProfileId: input.defaultChatRuntimeProfileId ?? null,
1125-
updatedAt: new Date().toISOString(),
1126-
})
1136+
.set(patch)
11271137
.where(eq(projects.id, id))
11281138
.run();
11291139
return findProjectById(id);

0 commit comments

Comments
 (0)