feat(FN-3393): add eval settings with scheduled eval project settings UI

Adds a scheduled eval settings feature (FN-3393) with a new `evalSettings` contract in core, type-safe numeric validation, a project settings UI section, and an API route for persisting the payload. The bulk of the changes are in the settings modal and supporting test coverage. Two smaller, unrelate

Fusion-Task-Id: FN-3393
This commit is contained in:
Fusion
2026-05-07 07:36:36 -07:00
committed by gsxdsm
parent a76f06b750
commit 78f62b18b6
11 changed files with 529 additions and 24 deletions

View File

@@ -713,6 +713,69 @@ describe("PUT /settings", () => {
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 8, autoMerge: false });
});
it("accepts valid nested evalSettings payload", async () => {
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
evalSettings: {
enabled: true,
intervalMs: 300000,
evaluatorProvider: "openai",
evaluatorModelId: "gpt-5",
followUpPolicy: "suggest-only",
retentionDays: 45,
},
});
const payload = {
evalSettings: {
enabled: true,
intervalMs: 300000,
evaluatorProvider: "openai",
evaluatorModelId: "gpt-5",
followUpPolicy: "suggest-only",
retentionDays: 45,
},
};
const res = await REQUEST(buildApp(), "PUT", "/api/settings", JSON.stringify(payload), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith(payload);
});
it("rejects invalid evalSettings payloads", async () => {
const invalidPayloads = [
{
payload: { evalSettings: { intervalMs: 59_999 } },
message: "evalSettings.intervalMs",
},
{
payload: { evalSettings: { retentionDays: 0 } },
message: "evalSettings.retentionDays",
},
{
payload: { evalSettings: { followUpPolicy: "create" } },
message: "evalSettings.followUpPolicy",
},
{
payload: { evalSettings: { evaluatorProvider: "openai" } },
message: "evalSettings.evaluatorProvider and evalSettings.evaluatorModelId",
},
];
for (const { payload, message } of invalidPayloads) {
const res = await REQUEST(buildApp(), "PUT", "/api/settings", JSON.stringify(payload), {
"Content-Type": "application/json",
});
expect(res.status).toBe(400);
expect(res.body.error).toContain(message);
}
expect(store.updateSettings).not.toHaveBeenCalled();
});
it("accepts partial remoteAccess patches and GET /settings returns merged sibling branches", async () => {
const mergedRemoteAccess = {
enabled: true,
@@ -1251,6 +1314,31 @@ describe("GET /settings/scopes", () => {
expect(res.body.project.titleSummarizerModelId).toBe("gemini-2.5-pro");
});
it("returns evalSettings in project scope only", async () => {
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockResolvedValue({
global: { themeMode: "dark" },
project: {
evalSettings: {
enabled: true,
intervalMs: 300000,
followUpPolicy: "auto-create",
retentionDays: 14,
},
},
});
const res = await GET(buildApp(), "/api/settings/scopes");
expect(res.status).toBe(200);
expect(res.body.project.evalSettings).toEqual({
enabled: true,
intervalMs: 300000,
followUpPolicy: "auto-create",
retentionDays: 14,
});
expect(res.body.global.evalSettings).toBeUndefined();
});
it("returns remoteAccess only under project scope", async () => {
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockResolvedValue({
global: { themeMode: "dark" },