feat(FN-1828): merge fusion/fn-1828

This commit is contained in:
gsxdsm
2026-04-14 13:30:02 -07:00
parent f9352cb672
commit b4917afdf9
10 changed files with 374 additions and 15 deletions

View File

@@ -1620,6 +1620,61 @@ export function SettingsModal({
</label>
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(KB-001): ...</code>)</small>
</div>
<div className="form-group">
<label htmlFor="commitAuthorEnabled" className="checkbox-label">
<input
id="commitAuthorEnabled"
type="checkbox"
checked={form.commitAuthorEnabled !== false}
onChange={(e) =>
setForm((f) => ({ ...f, commitAuthorEnabled: e.target.checked }))
}
/>
Add author attribution to commits
</label>
<small>
When enabled, all commits made by Fusion include <code>--author</code>{" "}
attribution identifying them as AI-generated
</small>
</div>
{form.commitAuthorEnabled !== false && (
<>
<div className="form-group">
<label htmlFor="commitAuthorName">Author Name</label>
<input
id="commitAuthorName"
type="text"
value={form.commitAuthorName ?? ""}
placeholder="Fusion"
onChange={(e) =>
setForm((f) => ({
...f,
commitAuthorName: e.target.value || undefined,
}))
}
/>
<small>Name used in commit author attribution</small>
</div>
<div className="form-group">
<label htmlFor="commitAuthorEmail">Author Email</label>
<input
id="commitAuthorEmail"
type="email"
value={form.commitAuthorEmail ?? ""}
placeholder="noreply@runfusion.ai"
onChange={(e) =>
setForm((f) => ({
...f,
commitAuthorEmail: e.target.value || undefined,
}))
}
/>
<small>Email used in commit author attribution</small>
</div>
</>
)}
<div className="form-group">
<label htmlFor="autoResolveConflicts" className="checkbox-label">
<input

View File

@@ -559,6 +559,102 @@ describe("SettingsModal", () => {
expect(payload.includeTaskIdInCommit).toBe(false);
});
it("shows Add author attribution to commits 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("Add author attribution to commits");
expect(checkbox).toBeTruthy();
expect(checkbox.getAttribute("type")).toBe("checkbox");
});
it("toggling commitAuthorEnabled 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("Add author attribution to commits");
// 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.commitAuthorEnabled).toBe(false);
});
it("shows author name and email fields when commitAuthorEnabled is true", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
// Author name and email fields should be visible when author attribution is enabled
expect(screen.getByLabelText("Author Name")).toBeTruthy();
expect(screen.getByLabelText("Author Email")).toBeTruthy();
});
it("hides author name and email fields when commitAuthorEnabled is false", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
commitAuthorEnabled: false,
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
// Author name and email fields should be hidden
expect(screen.queryByLabelText("Author Name")).toBeNull();
expect(screen.queryByLabelText("Author Email")).toBeNull();
});
it("sends custom author name and email in save payload", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
// Change author name
const nameInput = screen.getByLabelText("Author Name");
fireEvent.change(nameInput, { target: { value: "CustomBot" } });
// Change author email
const emailInput = screen.getByLabelText("Author Email");
fireEvent.change(emailInput, { target: { value: "bot@example.com" } });
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.commitAuthorName).toBe("CustomBot");
expect(payload.commitAuthorEmail).toBe("bot@example.com");
});
it("clears author name to undefined when input is emptied", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
commitAuthorName: "SomeBot",
commitAuthorEmail: "some@example.com",
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Merge"));
// Clear author name
const nameInput = screen.getByLabelText("Author Name");
fireEvent.change(nameInput, { target: { value: "" } });
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.commitAuthorName).toBeUndefined();
});
it("toggling autoResolveConflicts checkbox sends false in save payload when unchecked", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());