feat(KB-037): add smartConflictResolution setting toggle

- Add smartConflictResolution toggle to Merge section in SettingsModal\n- Implement checkbox UI with label and description\n- Add comprehensive tests for toggle behavior and state\n- Follow existing SettingsModal patterns for consistency
This commit is contained in:
gsxdsm
2026-03-29 19:30:18 -07:00
parent 4298f12419
commit 90e3f72c8e
2 changed files with 77 additions and 0 deletions

View File

@@ -472,6 +472,20 @@ export function SettingsModal({ onClose, addToast, initialSection }: SettingsMod
</label>
<small>When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.), generated files (dist/*, *.gen.ts), and trivial whitespace conflicts are resolved automatically without AI intervention. Complex code conflicts still require AI review.</small>
</div>
<div className="form-group">
<label htmlFor="smartConflictResolution" className="checkbox-label">
<input
id="smartConflictResolution"
type="checkbox"
checked={form.smartConflictResolution !== false}
onChange={(e) =>
setForm((f) => ({ ...f, smartConflictResolution: e.target.checked }))
}
/>
Smart conflict resolution
</label>
<small>When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.</small>
</div>
</>
);
case "authentication":

View File

@@ -14,6 +14,7 @@ const defaultSettings: Settings = {
testCommand: "",
buildCommand: "",
autoResolveConflicts: true,
smartConflictResolution: true,
};
vi.mock("../../api", () => ({
@@ -105,6 +106,7 @@ describe("SettingsModal", () => {
expect(screen.getByText("Auto-merge completed tasks")).toBeTruthy();
expect(screen.getByText("Include task ID in commit scope")).toBeTruthy();
expect(screen.getByText("Auto-resolve conflicts in lock files and generated files")).toBeTruthy();
expect(screen.getByText("Smart conflict resolution")).toBeTruthy();
});
it("shows Recycle worktrees checkbox in Worktrees section", async () => {
@@ -249,6 +251,67 @@ describe("SettingsModal", () => {
expect(checkbox.checked).toBe(true);
});
it("shows Smart conflict resolution checkbox in Merge section", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
const checkbox = screen.getByLabelText("Smart conflict resolution");
expect(checkbox).toBeTruthy();
expect(checkbox.getAttribute("type")).toBe("checkbox");
});
it("toggling smartConflictResolution checkbox sends false in save payload when unchecked", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
const checkbox = screen.getByLabelText("Smart conflict resolution");
// Default is checked (true), click to uncheck
fireEvent.click(checkbox);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(payload.smartConflictResolution).toBe(false);
});
it("smartConflictResolution defaults to enabled (true) when setting is true", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
smartConflictResolution: true,
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
const checkbox = screen.getByLabelText("Smart conflict resolution") as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});
it("smartConflictResolution checkbox submits true in save payload when checked", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
smartConflictResolution: false,
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
const checkbox = screen.getByLabelText("Smart conflict resolution");
// Default is unchecked (false), click to check
fireEvent.click(checkbox);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(payload.smartConflictResolution).toBe(true);
});
it("groupOverlappingFiles input has type checkbox", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());