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:
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user