Skip to content

Commit 65ed040

Browse files
authored
fix(tasks): detect PR on local task's current branch (#2779)
1 parent eeb68ed commit 65ed040

2 files changed

Lines changed: 145 additions & 10 deletions

File tree

packages/workspace-server/src/services/git/task-pr-status.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,135 @@ describe("TaskPrStatusService.getTaskPrStatus (missing worktree directory)", ()
5151
expect(gitService.getDiffStats).not.toHaveBeenCalled();
5252
});
5353
});
54+
55+
describe("TaskPrStatusService revalidation PR detection", () => {
56+
let service: TaskPrStatusService;
57+
let gitService: {
58+
getPrStatus: ReturnType<typeof vi.fn>;
59+
getDiffStats: ReturnType<typeof vi.fn>;
60+
getGitSyncStatus: ReturnType<typeof vi.fn>;
61+
getPrUrlForBranch: ReturnType<typeof vi.fn>;
62+
getPrDetailsByUrl: ReturnType<typeof vi.fn>;
63+
};
64+
let workspaceService: {
65+
getWorkspace: ReturnType<typeof vi.fn>;
66+
emit: ReturnType<typeof vi.fn>;
67+
};
68+
let workspaceRepo: {
69+
findByTaskId: ReturnType<typeof vi.fn>;
70+
updatePrCache: ReturnType<typeof vi.fn>;
71+
};
72+
73+
beforeEach(() => {
74+
vi.restoreAllMocks();
75+
gitService = {
76+
getPrStatus: vi.fn(),
77+
getDiffStats: vi.fn().mockResolvedValue({ filesChanged: 0 }),
78+
getGitSyncStatus: vi.fn().mockResolvedValue({ aheadOfDefault: 0 }),
79+
getPrUrlForBranch: vi.fn(),
80+
getPrDetailsByUrl: vi.fn(),
81+
};
82+
workspaceService = { getWorkspace: vi.fn(), emit: vi.fn() };
83+
workspaceRepo = {
84+
findByTaskId: vi.fn().mockReturnValue({ prUrl: null, prState: null }),
85+
updatePrCache: vi.fn(),
86+
};
87+
service = new TaskPrStatusService(
88+
gitService as unknown as GitService,
89+
workspaceRepo as unknown as IWorkspaceRepository,
90+
workspaceService as unknown as WorkspaceService,
91+
);
92+
vi.spyOn(fs, "existsSync").mockReturnValue(true);
93+
});
94+
95+
it.each([
96+
{
97+
name: "detects a PR on a local task's current branch with no linked branch",
98+
taskId: "task-local",
99+
workspace: { mode: "local", worktreePath: null, folderPath: "/repo" },
100+
prStatus: {
101+
prExists: true,
102+
prState: "open",
103+
prUrl: "https://github.com/acme/repo/pull/7",
104+
isDraft: false,
105+
},
106+
diffStats: { filesChanged: 0 },
107+
expectedRepoPath: "/repo",
108+
expectDiffStatsCalled: false,
109+
expectedCache: {
110+
prUrl: "https://github.com/acme/repo/pull/7",
111+
prState: "open",
112+
},
113+
expectedEmit: {
114+
prUrl: "https://github.com/acme/repo/pull/7",
115+
prState: "open",
116+
},
117+
},
118+
{
119+
name: "caches no PR for a local task whose branch has none, without checking diff",
120+
taskId: "task-local",
121+
workspace: { mode: "local", worktreePath: null, folderPath: "/repo" },
122+
prStatus: { prExists: false },
123+
diffStats: { filesChanged: 0 },
124+
expectedRepoPath: "/repo",
125+
expectDiffStatsCalled: false,
126+
expectedCache: { prUrl: null, prState: null },
127+
expectedEmit: null,
128+
},
129+
{
130+
name: "still reports a worktree task's local diff when no PR exists",
131+
taskId: "task-wt",
132+
workspace: { mode: "worktree", worktreePath: "/wt", folderPath: null },
133+
prStatus: { prExists: false },
134+
diffStats: { filesChanged: 3 },
135+
expectedRepoPath: "/wt",
136+
expectDiffStatsCalled: true,
137+
expectedCache: { prUrl: null, prState: null },
138+
expectedEmit: null,
139+
},
140+
])(
141+
"$name",
142+
async ({
143+
taskId,
144+
workspace,
145+
prStatus,
146+
diffStats,
147+
expectedRepoPath,
148+
expectDiffStatsCalled,
149+
expectedCache,
150+
expectedEmit,
151+
}) => {
152+
workspaceService.getWorkspace.mockResolvedValue({
153+
...workspace,
154+
linkedBranch: null,
155+
});
156+
gitService.getPrStatus.mockResolvedValue(prStatus);
157+
gitService.getDiffStats.mockResolvedValue(diffStats);
158+
159+
await service.getTaskPrStatus(taskId, null);
160+
await new Promise((resolve) => setImmediate(resolve));
161+
162+
expect(gitService.getPrStatus).toHaveBeenCalledWith(expectedRepoPath);
163+
if (expectDiffStatsCalled) {
164+
expect(gitService.getDiffStats).toHaveBeenCalledWith(expectedRepoPath);
165+
} else {
166+
expect(gitService.getDiffStats).not.toHaveBeenCalled();
167+
}
168+
expect(workspaceRepo.updatePrCache).toHaveBeenCalledWith(
169+
taskId,
170+
expectedCache,
171+
);
172+
if (expectedEmit) {
173+
expect(workspaceService.emit).toHaveBeenCalledWith(
174+
"taskPrInfoChanged",
175+
{
176+
taskId,
177+
...expectedEmit,
178+
},
179+
);
180+
} else {
181+
expect(workspaceService.emit).not.toHaveBeenCalled();
182+
}
183+
},
184+
);
185+
});

packages/workspace-server/src/services/git/task-pr-status.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ export class TaskPrStatusService {
156156
return { prUrl: null, prState: null, hasDiff: false };
157157
}
158158

159-
if (worktreePath) {
160-
const prStatus = await this.gitService.getPrStatus(worktreePath);
159+
if (repoPath) {
160+
const prStatus = await this.gitService.getPrStatus(repoPath);
161161
if (prStatus.prExists && prStatus.prState) {
162162
return {
163163
prUrl: prStatus.prUrl,
@@ -170,16 +170,19 @@ export class TaskPrStatusService {
170170
};
171171
}
172172

173-
const [diffStats, syncStatus] = await Promise.all([
174-
this.gitService.getDiffStats(worktreePath),
175-
this.gitService.getGitSyncStatus(worktreePath),
176-
]);
173+
// Only worktree tasks track local diff/ahead state as a PR-less signal.
174+
if (worktreePath) {
175+
const [diffStats, syncStatus] = await Promise.all([
176+
this.gitService.getDiffStats(worktreePath),
177+
this.gitService.getGitSyncStatus(worktreePath),
178+
]);
177179

178-
const hasDiff =
179-
(diffStats?.filesChanged ?? 0) > 0 ||
180-
(syncStatus?.aheadOfDefault ?? 0) > 0;
180+
const hasDiff =
181+
(diffStats?.filesChanged ?? 0) > 0 ||
182+
(syncStatus?.aheadOfDefault ?? 0) > 0;
181183

182-
return { prUrl: null, prState: null, hasDiff };
184+
return { prUrl: null, prState: null, hasDiff };
185+
}
183186
}
184187

185188
return { prUrl: null, prState: null, hasDiff: false };

0 commit comments

Comments
 (0)