feat(FN-4032): add tracking retry with actionable create issue error in tas

The merge adds a GitHub integration retry lifecycle to the task tracking system, including an actionable retry mechanism for failed issue creation, fixes to tracking and auth retry flow, and corresponding tests in the dashboard and engine packages. Documentation is updated in `docs/architecture.md`

Fusion-Task-Id: FN-4032
This commit is contained in:
Fusion
2026-05-11 16:27:40 -07:00
committed by gsxdsm
parent 351702d055
commit 2ccf8c699d
10 changed files with 177 additions and 42 deletions

View File

@@ -837,6 +837,24 @@ export function TaskDetailContent({
}
}, [addToast, canEditGithubTracking, githubRepoOverrideTrimmed, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
const handleRetryGithubTrackingIssueCreate = useCallback(async () => {
if (!githubTrackingEnabled || githubTrackedIssue || isSavingGithubTracking) return;
setIsSavingGithubTracking(true);
try {
const updatedTask = await updateTask(task.id, {
githubTracking: {
enabled: true,
},
}, projectId);
onTaskUpdated?.(updatedTask);
addToast("Requested GitHub tracking issue creation", "info");
} catch (err) {
addToast(`Failed to update ${task.id}: ${getErrorMessage(err)}`, "error");
} finally {
if (mountedRef.current) setIsSavingGithubTracking(false);
}
}, [addToast, githubTrackedIssue, githubTrackingEnabled, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
const enterEditMode = useCallback(() => {
if (!canEdit) return;
setIsEditing(true);
@@ -2399,40 +2417,47 @@ export function TaskDetailContent({
</div>
</dl>
)}
{canEditGithubTracking && (
<div className="detail-github-tracking-controls">
<label className="checkbox-label" htmlFor="detail-github-tracking-toggle">
<input
id="detail-github-tracking-toggle"
type="checkbox"
checked={githubTrackingEnabled}
disabled={isSavingGithubTracking}
onChange={() => void handleToggleGithubTracking()}
/>
Enable GitHub tracking
</label>
<div className="detail-github-tracking-repo-row">
<input
className="input"
value={githubRepoOverrideDraft}
onChange={(event) => {
setGithubRepoOverrideDraft(event.target.value);
setGithubRepoOverrideError(null);
}}
placeholder={effectiveGithubRepoDefault || "owner/repo"}
/>
<button className="btn btn-sm" onClick={() => void handleSaveGithubRepoOverride()} disabled={isSavingGithubTracking}>
Save
</button>
</div>
{githubRepoOverrideError && <small className="detail-github-tracking-error">{githubRepoOverrideError}</small>}
{githubTrackedIssue && (
<button className="btn btn-sm touch-target" onClick={() => void handleUnlinkGithubIssue()} disabled={isSavingGithubTracking}>
Unlink GitHub issue
</button>
)}
</div>
)}
<div className="detail-github-tracking-controls">
{!githubTrackedIssue && githubTrackingEnabled && (
<button className="btn btn-sm touch-target" onClick={() => void handleRetryGithubTrackingIssueCreate()} disabled={isSavingGithubTracking}>
Create tracking issue
</button>
)}
{canEditGithubTracking && (
<>
<label className="checkbox-label" htmlFor="detail-github-tracking-toggle">
<input
id="detail-github-tracking-toggle"
type="checkbox"
checked={githubTrackingEnabled}
disabled={isSavingGithubTracking}
onChange={() => void handleToggleGithubTracking()}
/>
Enable GitHub tracking
</label>
<div className="detail-github-tracking-repo-row">
<input
className="input"
value={githubRepoOverrideDraft}
onChange={(event) => {
setGithubRepoOverrideDraft(event.target.value);
setGithubRepoOverrideError(null);
}}
placeholder={effectiveGithubRepoDefault || "owner/repo"}
/>
<button className="btn btn-sm" onClick={() => void handleSaveGithubRepoOverride()} disabled={isSavingGithubTracking}>
Save
</button>
</div>
{githubRepoOverrideError && <small className="detail-github-tracking-error">{githubRepoOverrideError}</small>}
{githubTrackedIssue && (
<button className="btn btn-sm touch-target" onClick={() => void handleUnlinkGithubIssue()} disabled={isSavingGithubTracking}>
Unlink GitHub issue
</button>
)}
</>
)}
</div>
</div>
)}
</div>

View File

@@ -2222,6 +2222,32 @@ describe("TaskDetailModal", () => {
expect(screen.queryByText("GitHub tracking")).toBeNull();
});
it("shows create tracking issue action for enabled but unlinked tasks outside editable columns", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
render(
<TaskDetailModal
task={makeTask({ id: "FN-001", column: "done", githubTracking: { enabled: true } })}
onClose={noop}
onOpenDetail={noopOpenDetail}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
expandGithubTracking();
fireEvent.click(screen.getByRole("button", { name: "Create tracking issue" }));
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: true } }, undefined);
});
expect(screen.queryByLabelText("Enable GitHub tracking")).toBeNull();
});
it("sends githubTracking disabled→enabled toggle payload", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);