feat(FN-3891): expose verification auto-fix retries in merge settings
- Add verificationFixRetries input to SettingsModal merge section with 0-3 clamping - Default to 3 retries with proper empty-value fallback to undefined - Cover edge cases with SettingsModal tests: default, valid values, clamping, clearing - Add patch changeset for @runfusion/fusion - Update settings-reference.md with revised default and description Fusion-Task-Id: FN-3891
This commit is contained in:
5
.changeset/FN-3891-verification-fix-retries-setting.md
Normal file
5
.changeset/FN-3891-verification-fix-retries-setting.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Expose verificationFixRetries (0-3) in Settings → Merge so users can tune in-merge auto-fix attempts without editing JSON.
|
||||
@@ -209,7 +209,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `mergerAutostashMaxAgeHours` | `number` | `24` | Maximum autostash age in hours before startup/periodic stale-stash sweep drops `fusion-merger-autostash:*` leftovers (minimum `1`). |
|
||||
| `strictScopeEnforcement` | `boolean` | `false` | Block merges on out-of-scope file changes. |
|
||||
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
|
||||
| `verificationFixRetries` | `number` | `2` | Auto-fix retry attempts when verification fails during merge. |
|
||||
| `verificationFixRetries` | `number` | `3` | In-merge auto-fix retry attempts after deterministic test/build verification failures (0-3). |
|
||||
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
|
||||
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
|
||||
| `agentProvisioning` | `{ approvalMode?: "always" \| "trusted-only" \| "never"; trustedRoles?: string[]; trustedAgentIds?: string[]; alwaysApproveDelete?: boolean }` | `{}` | Approval policy for `fn_agent_create`/`fn_agent_delete` (`approvalMode` default `trusted-only`, delete approvals default on via `alwaysApproveDelete: true`). |
|
||||
|
||||
@@ -3560,19 +3560,38 @@ export function SettingsModal({
|
||||
</details>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="verificationFixRetries">Verification fix retries</label>
|
||||
<label htmlFor="verificationFixRetries">Verification auto-fix retries</label>
|
||||
<input
|
||||
id="verificationFixRetries"
|
||||
className="input"
|
||||
type="number"
|
||||
min={0}
|
||||
max={3}
|
||||
value={form.verificationFixRetries ?? ""}
|
||||
step={1}
|
||||
value={form.verificationFixRetries ?? 3}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm((f) => ({ ...f, verificationFixRetries: val === "" ? undefined : Number(val) } as SettingsFormState));
|
||||
const rawValue = e.target.value;
|
||||
if (rawValue === "") {
|
||||
setForm((f) => ({ ...f, verificationFixRetries: undefined } as SettingsFormState));
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedValue = Number.parseInt(rawValue, 10);
|
||||
if (!Number.isFinite(parsedValue)) {
|
||||
setForm((f) => ({ ...f, verificationFixRetries: undefined } as SettingsFormState));
|
||||
return;
|
||||
}
|
||||
|
||||
const clampedValue = Math.max(0, Math.min(3, parsedValue));
|
||||
setForm((f) => ({ ...f, verificationFixRetries: clampedValue } as SettingsFormState));
|
||||
}}
|
||||
/>
|
||||
<small>Number of automatic fix attempts after failed merge verification (0–3)</small>
|
||||
<details className="settings-option-details">
|
||||
<summary>More details</summary>
|
||||
<small>
|
||||
Controls in-merge fix attempts after deterministic test/build verification failures (0-3).
|
||||
</small>
|
||||
</details>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="mergeStrategy">Auto-completion mode</label>
|
||||
|
||||
@@ -1896,23 +1896,65 @@ describe("SettingsModal", () => {
|
||||
expect(payload.pushRemote).toBe("upstream main");
|
||||
});
|
||||
|
||||
it("renders and saves verification fix retries", async () => {
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
describe("verificationFixRetries", () => {
|
||||
it("shows default value 3 when verificationFixRetries is not set", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
verificationFixRetries: undefined,
|
||||
});
|
||||
|
||||
const retriesInput = screen.getByLabelText("Verification fix retries") as HTMLInputElement;
|
||||
expect(retriesInput.value).toBe("2");
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
await userEvent.clear(retriesInput);
|
||||
await userEvent.type(retriesInput, "1");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalled();
|
||||
const retriesInput = screen.getByLabelText("Verification auto-fix retries") as HTMLInputElement;
|
||||
expect(retriesInput.value).toBe("3");
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(payload.verificationFixRetries).toBe(1);
|
||||
it.each([0, 1, 2, 3])("persists valid value %i", async (value) => {
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const retriesInput = screen.getByLabelText("Verification auto-fix retries") as HTMLInputElement;
|
||||
fireEvent.change(retriesInput, { target: { value: String(value) } });
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(payload.verificationFixRetries).toBe(value);
|
||||
});
|
||||
|
||||
it("clamps out-of-range values", async () => {
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const retriesInput = screen.getByLabelText("Verification auto-fix retries") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(retriesInput, { target: { value: "5" } });
|
||||
expect(retriesInput.value).toBe("3");
|
||||
|
||||
fireEvent.change(retriesInput, { target: { value: "-1" } });
|
||||
expect(retriesInput.value).toBe("0");
|
||||
});
|
||||
|
||||
it("saving after clearing input persists undefined and falls back to visible default 3", async () => {
|
||||
renderModal({ initialSection: "merge" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const retriesInput = screen.getByLabelText("Verification auto-fix retries") as HTMLInputElement;
|
||||
await userEvent.clear(retriesInput);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(payload.verificationFixRetries).toBeUndefined();
|
||||
expect(retriesInput.value).toBe("3");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders and saves github issue tracking controls", async () => {
|
||||
|
||||
Reference in New Issue
Block a user