Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/api/http/git/operations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from "vitest";

import { BRANCH_REMOTE_MUTATION_EVENT } from "@src/util/git/branchRemoteMutation";

import { fetchRustApi } from "./client";
import { gitPush } from "./operations";

vi.mock("./client", () => ({
fetchRustApi: vi.fn(),
gitRepoUrl: (repoId: string) => `/git/repos/${repoId}`,
}));

const fetchRustApiMock = vi.mocked(fetchRustApi);

describe("gitPush", () => {
afterEach(() => {
vi.clearAllMocks();
});

it("announces the pushed repo and branch after success", async () => {
fetchRustApiMock.mockResolvedValue({ data: { success: true } } as never);
const details: unknown[] = [];
const listener = (event: Event) => {
details.push((event as CustomEvent).detail);
};
window.addEventListener(BRANCH_REMOTE_MUTATION_EVENT, listener);

try {
await gitPush({
repo_id: "repo-1",
repo_path: "/repo",
branch: "feature",
});
} finally {
window.removeEventListener(BRANCH_REMOTE_MUTATION_EVENT, listener);
}

expect(details).toEqual([
{
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
reason: "push",
},
]);
});
});
9 changes: 9 additions & 0 deletions src/api/http/git/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*
* Fetch, pull, and push operations.
*/
import { announceBranchRemoteMutation } from "@src/util/git/branchRemoteMutation";

import { fetchRustApi, gitRepoUrl } from "./client";
import type { GitErrorType } from "./streaming";
import type { GitOperationResponse, GitPullResponse } from "./types";
Expand Down Expand Up @@ -150,5 +152,12 @@ export const gitPush = async (params: {
throwGitRemoteOperationError(result, "Push failed");
}

announceBranchRemoteMutation({
repoId: params.repo_id,
repoPath: params.repo_path,
branchName: params.branch,
reason: "push",
});

return result;
};
183 changes: 183 additions & 0 deletions src/hooks/git/useBranchPullRequestStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import {
import {
BRANCH_CI_POLL_BASE_MS,
BRANCH_CI_POLL_MAX_MS,
BRANCH_CI_SAFETY_POLL_MS,
clearBranchPullRequestStatusCache,
} from "@src/services/git/branchPullRequestStatus";
import { announceBranchRemoteMutation } from "@src/util/git/branchRemoteMutation";

import {
type UseBranchPullRequestStatusOptions,
Expand Down Expand Up @@ -339,6 +341,187 @@ describe("useBranchPullRequestStatus", () => {
expect(getChecksLocalMock).toHaveBeenCalledTimes(3);
});

it("discovers a newly-created PR immediately after branch invalidation", async () => {
findPullRequestLocalMock.mockResolvedValueOnce(null);
let latest!: UseBranchPullRequestStatusResult;

await act(async () => {
root.render(
createElement(Probe, {
options: {
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
poll: true,
},
onValue: (value) => {
latest = value;
},
})
);
});
expect(latest.pr).toBeNull();

await act(async () => {
announceBranchRemoteMutation({
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
reason: "pull-request-created",
});
});

expect(findPullRequestLocalMock).toHaveBeenCalledTimes(2);
expect(latest.pr?.number).toBe(12);
expect(latest.ciStatus).toBe("success");
});

it("defers a hidden push invalidation and forces it on visibility return", async () => {
getPRLocalMock
.mockResolvedValueOnce({ head: { sha: "abc" } })
.mockResolvedValueOnce({ head: { sha: "def" } });
getChecksLocalMock
.mockResolvedValueOnce({
...runningChecks(),
sha: "abc",
state: "success",
check_runs: [
{
...runningChecks().check_runs[0],
status: "completed",
conclusion: "success",
},
],
})
.mockResolvedValueOnce({ ...runningChecks(), sha: "def" });
let latest!: UseBranchPullRequestStatusResult;

await act(async () => {
root.render(
createElement(Probe, {
options: {
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
poll: true,
},
onValue: (value) => {
latest = value;
},
})
);
});
expect(latest.ciStatus).toBe("success");

visibilityState = "hidden";
document.dispatchEvent(new Event("visibilitychange"));
await act(async () => {
announceBranchRemoteMutation({
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
reason: "push",
});
});
expect(getChecksLocalMock).toHaveBeenCalledTimes(1);

visibilityState = "visible";
await act(async () => {
document.dispatchEvent(new Event("visibilitychange"));
});

expect(getChecksLocalMock).toHaveBeenLastCalledWith("acme/repo", "def");
expect(latest.ciStatus).toBe("pending");
});

it("forces a new PR-head read when local HEAD changes", async () => {
getPRLocalMock
.mockResolvedValueOnce({ head: { sha: "abc" } })
.mockResolvedValueOnce({ head: { sha: "def" } });
getChecksLocalMock
.mockResolvedValueOnce({
...runningChecks(),
sha: "abc",
state: "success",
check_runs: [
{
...runningChecks().check_runs[0],
status: "completed",
conclusion: "success",
},
],
})
.mockResolvedValueOnce({ ...runningChecks(), sha: "def" });
let latest!: UseBranchPullRequestStatusResult;
const onValue = (value: UseBranchPullRequestStatusResult) => {
latest = value;
};

await act(async () => {
root.render(
createElement(Probe, {
options: {
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
headRevision: "abc1234",
poll: true,
},
onValue,
})
);
});
expect(latest.ciStatus).toBe("success");

await act(async () => {
root.render(
createElement(Probe, {
options: {
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
headRevision: "def5678",
poll: true,
},
onValue,
})
);
});

expect(getPRLocalMock).toHaveBeenCalledTimes(2);
expect(getChecksLocalMock).toHaveBeenLastCalledWith("acme/repo", "def");
expect(latest.ciStatus).toBe("pending");
});

it("uses only a slow safety refresh after settled CI", async () => {
vi.useFakeTimers();

await act(async () => {
root.render(
createElement(Probe, {
options: {
repoId: "repo-1",
repoPath: "/repo",
branchName: "feature",
poll: true,
},
onValue: () => undefined,
})
);
});
expect(getChecksLocalMock).toHaveBeenCalledTimes(1);

await act(async () => {
await vi.advanceTimersByTimeAsync(BRANCH_CI_SAFETY_POLL_MS - 1);
});
expect(getChecksLocalMock).toHaveBeenCalledTimes(1);

await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(getChecksLocalMock).toHaveBeenCalledTimes(2);
});

it("never schedules a poll when tracing is not requested", async () => {
vi.useFakeTimers();
getChecksLocalMock.mockResolvedValue(runningChecks());
Expand Down
Loading
Loading