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`). |
|
| `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. |
|
| `strictScopeEnforcement` | `boolean` | `false` | Block merges on out-of-scope file changes. |
|
||||||
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
|
| `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). |
|
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
|
||||||
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
|
| `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`). |
|
| `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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="verificationFixRetries">Verification fix retries</label>
|
<label htmlFor="verificationFixRetries">Verification auto-fix retries</label>
|
||||||
<input
|
<input
|
||||||
id="verificationFixRetries"
|
id="verificationFixRetries"
|
||||||
|
className="input"
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={3}
|
max={3}
|
||||||
value={form.verificationFixRetries ?? ""}
|
step={1}
|
||||||
|
value={form.verificationFixRetries ?? 3}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value;
|
const rawValue = e.target.value;
|
||||||
setForm((f) => ({ ...f, verificationFixRetries: val === "" ? undefined : Number(val) } as SettingsFormState));
|
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>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeStrategy">Auto-completion mode</label>
|
<label htmlFor="mergeStrategy">Auto-completion mode</label>
|
||||||
|
|||||||
@@ -1896,23 +1896,65 @@ describe("SettingsModal", () => {
|
|||||||
expect(payload.pushRemote).toBe("upstream main");
|
expect(payload.pushRemote).toBe("upstream main");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders and saves verification fix retries", async () => {
|
describe("verificationFixRetries", () => {
|
||||||
renderModal({ initialSection: "merge" });
|
it("shows default value 3 when verificationFixRetries is not set", async () => {
|
||||||
await waitForSettingsModalReady();
|
mockFetchSettings.mockResolvedValueOnce({
|
||||||
|
...defaultSettings,
|
||||||
|
verificationFixRetries: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const retriesInput = screen.getByLabelText("Verification fix retries") as HTMLInputElement;
|
renderModal({ initialSection: "merge" });
|
||||||
expect(retriesInput.value).toBe("2");
|
await waitForSettingsModalReady();
|
||||||
|
|
||||||
await userEvent.clear(retriesInput);
|
const retriesInput = screen.getByLabelText("Verification auto-fix retries") as HTMLInputElement;
|
||||||
await userEvent.type(retriesInput, "1");
|
expect(retriesInput.value).toBe("3");
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
it.each([0, 1, 2, 3])("persists valid value %i", async (value) => {
|
||||||
expect(payload.verificationFixRetries).toBe(1);
|
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 () => {
|
it("renders and saves github issue tracking controls", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user