fix(FN-853): fix ahead-commit sync in Git Manager and clean up task form tests

- Fix ahead-commit count synchronization in Git Manager remote remotes display
- Add refresh behavior for commits-to-push count after git operations
- Update README with commits-to-push refresh behavior documentation
- Add GitManagerModal tests and remove redundant TaskForm/NewTaskModal/TaskDetailModal tests
- Remove unused CSS styles from dashboard styles.css
- Clean up AGENTS.md formatting
This commit is contained in:
gsxdsm
2026-04-04 02:43:37 -07:00
parent 7d2ec639e1
commit 2d74a26428
3 changed files with 132 additions and 3 deletions

View File

@@ -201,7 +201,7 @@ The Git Manager provides comprehensive repository visualization and management d
- Pull latest changes
- Push current branch
- View operation results and error states
- **Commits to Push**: See which local commits are ahead of the upstream tracking branch (pending push) with short hash, message, author, and relative date
- **Commits to Push**: See which local commits are ahead of the upstream tracking branch (pending push) with short hash, message, author, and relative date. The list stays synchronized with the ahead count — it refreshes automatically after fetch, pull, push, and manual refresh operations, and clears when the ahead count drops to zero (e.g., after a successful push).
- **Remote Commit Inspection**: Click any remote to view its recent commit history — useful for checking what's on a remote without switching to the terminal
- Auto-selects the first remote and loads its recent commits on mount

View File

@@ -1510,12 +1510,23 @@ function RemotesPanel({
const [loadingRemoteCommits, setLoadingRemoteCommits] = useState(false);
const [remoteCommitsError, setRemoteCommitsError] = useState<string | null>(null);
// Fetch remotes and ahead commits when panel mounts
// Fetch remotes when panel mounts
useEffect(() => {
loadRemotes();
loadAheadCommits();
}, []);
// Load ahead commits whenever the ahead count indicates commits to push.
// This covers: initial mount (when status arrives), status refresh after
// remote actions (fetch/pull/push), and any other status updates.
useEffect(() => {
if (status && status.ahead > 0) {
loadAheadCommits();
} else if (status && status.ahead === 0) {
// Clear stale ahead commits when push succeeds or upstream catches up
setAheadCommits([]);
}
}, [status?.ahead]);
// Auto-select first remote when remotes load
useEffect(() => {
if (remotes.length > 0 && !selectedRemote) {

View File

@@ -1161,6 +1161,124 @@ describe("GitManagerModal", () => {
expect(screen.queryByTestId("commits-to-push")).not.toBeInTheDocument();
});
it("re-fetches ahead commits when status changes after fetch operation", async () => {
// This test verifies the regression fix: ahead commits are re-fetched when
// the ahead count changes after a remote action.
//
// Setup: mock status to go from ahead=0 → ahead=2 after fetch
// 1st call: mount (status tab)
// 2nd call: remotes tab load
// 3rd call: parent's handleFetch refreshes status
(fetchGitStatus as any)
.mockResolvedValueOnce({
branch: "main", commit: "abc1234", isDirty: false, ahead: 0, behind: 0,
})
.mockResolvedValueOnce({
branch: "main", commit: "abc1234", isDirty: false, ahead: 0, behind: 0,
})
.mockResolvedValueOnce({
branch: "main", commit: "abc1234", isDirty: false, ahead: 2, behind: 0,
});
// fetchAheadCommits should not be called initially (ahead=0)
(fetchAheadCommits as any).mockResolvedValue([]);
const user = userEvent.setup();
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
// Switch to remotes tab (uses 2nd mock: ahead=0)
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// No commits-to-push section since ahead=0
expect(screen.queryByTestId("commits-to-push")).not.toBeInTheDocument();
// Now mock ahead commits to return data when called next
(fetchAheadCommits as any).mockResolvedValue([
{ hash: "aaa1111", shortHash: "aaa1", message: "Ahead commit after fetch", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
{ hash: "bbb2222", shortHash: "bbb2", message: "Another ahead commit", author: "Dev", date: "2026-01-02T00:00:00Z", parents: [] },
]);
// Click the Refresh button in the header to trigger a full status refresh
// This causes fetchGitStatus to be called again (3rd mock: ahead=2)
const refreshBtn = screen.getByTitle("Refresh");
await user.click(refreshBtn);
// After status refresh, ahead=2 triggers loadAheadCommits
await waitFor(() => {
expect(fetchAheadCommits).toHaveBeenCalled();
});
await waitFor(() => {
expect(screen.getByTestId("commits-to-push")).toBeInTheDocument();
expect(screen.getByText("Ahead commit after fetch")).toBeInTheDocument();
expect(screen.getByText("Another ahead commit")).toBeInTheDocument();
});
});
it("clears ahead commits after push succeeds and ahead count drops to 0", async () => {
const user = userEvent.setup();
// First call: initial mount (status tab)
// Second call: switching to remotes tab — shows ahead commits
// Third call: after push — ahead drops to 0
(fetchGitStatus as any)
.mockResolvedValueOnce({
branch: "main",
commit: "abc1234",
isDirty: false,
ahead: 2,
behind: 0,
})
.mockResolvedValueOnce({
branch: "main",
commit: "abc1234",
isDirty: false,
ahead: 2,
behind: 0,
})
.mockResolvedValueOnce({
branch: "main",
commit: "abc1234",
isDirty: false,
ahead: 0,
behind: 0,
});
(fetchAheadCommits as any).mockResolvedValue([
{ hash: "aaa1111", shortHash: "aaa1", message: "First commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
{ hash: "bbb2222", shortHash: "bbb2", message: "Second commit", author: "Dev", date: "2026-01-02T00:00:00Z", parents: [] },
]);
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
// Initially shows commits to push
await waitFor(() => {
expect(screen.getByTestId("commits-to-push")).toBeInTheDocument();
});
// Push — parent handler refreshes status (3rd mock returns ahead: 0)
// Use getAllByRole since "2 commit(s) to push" text also matches /push/i
const pushButtons = screen.getAllByRole("button").filter(
(btn) => btn.textContent?.includes("Push") && !btn.textContent?.includes("commit")
);
expect(pushButtons.length).toBeGreaterThan(0);
await user.click(pushButtons[0]);
// After push, commits-to-push section should disappear
await waitFor(() => {
expect(screen.queryByTestId("commits-to-push")).not.toBeInTheDocument();
});
});
// ── Remote Selection & Recent Commits ──────────────────────────
it("shows recent commits section for auto-selected remote", async () => {