feat(FN-4116): fix github tracking issue creation
Fixes GitHub CLI issue creation by repairing the `gh issue create` call in the dashboard and adds logging for tracking failures, with tests covering both the core GitHub module and the tracking layer. Fusion-Task-Id: FN-4116
This commit is contained in:
@@ -223,4 +223,29 @@ describe("maybeCreateTrackingIssue", () => {
|
||||
|
||||
expect(resolveAuthMock).toHaveBeenCalledWith(expect.objectContaining({ globalSettings }));
|
||||
});
|
||||
|
||||
it("records activity when GitHub issue creation fails", async () => {
|
||||
const recordActivity = vi.fn();
|
||||
createIssueMock.mockRejectedValue(new Error("gh create failed"));
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ title: "Test", githubTracking: { enabled: true } }), {
|
||||
taskStore: { recordActivity, linkGithubIssue: vi.fn() } as any,
|
||||
projectSettings: {},
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
logger: { warn: vi.fn(), info: vi.fn() },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false, reason: "github_error" });
|
||||
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "task:updated",
|
||||
taskId: "FN-1",
|
||||
taskTitle: "Test",
|
||||
details: "GitHub tracking issue not created: gh create failed",
|
||||
metadata: expect.objectContaining({
|
||||
type: "github-issue-failed",
|
||||
reason: "github_error",
|
||||
message: "gh create failed",
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,16 +66,9 @@ describe("GitHubClient", () => {
|
||||
});
|
||||
|
||||
describe("createIssue", () => {
|
||||
it("uses gh path when authenticated", async () => {
|
||||
mockRunGhJsonAsync.mockResolvedValue({ url: "https://github.com/o/r/issues/8", number: 8, createdAt: "2026-01-02T00:00:00Z" } as any);
|
||||
const issue = await client.createIssue({ owner: "o", repo: "r", title: "t", body: "b", labels: ["bug"] });
|
||||
expect(mockRunGhJsonAsync).toHaveBeenCalled();
|
||||
expect(issue.number).toBe(8);
|
||||
});
|
||||
|
||||
it("falls back to API when gh path fails and token is configured", async () => {
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
mockRunGhJsonAsync.mockRejectedValue(new Error("gh failed"));
|
||||
mockRunGhAsync.mockRejectedValue(new Error("gh failed"));
|
||||
const fetchSpy = vi.spyOn(global, "fetch" as any).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 201,
|
||||
@@ -84,7 +77,8 @@ describe("GitHubClient", () => {
|
||||
|
||||
const issue = await clientWithToken.createIssue({ owner: "o", repo: "r", title: "t", body: "b" });
|
||||
expect(issue).toEqual({ owner: "o", repo: "r", number: 7, htmlUrl: "https://github.com/o/r/issues/7", createdAt: "2026-01-01T00:00:00Z" });
|
||||
expect(mockRunGhJsonAsync).toHaveBeenCalled();
|
||||
expect(mockRunGhAsync).toHaveBeenCalled();
|
||||
expect(mockRunGhJsonAsync).not.toHaveBeenCalled();
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -122,6 +116,82 @@ describe("GitHubClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubClient.createIssue (gh-cli mode)", () => {
|
||||
it("creates an issue from gh stdout and follow-up view metadata", async () => {
|
||||
mockRunGhAsync.mockResolvedValue("https://github.com/acme/repo/issues/42\n");
|
||||
mockRunGhJsonAsync.mockResolvedValue({
|
||||
number: 42,
|
||||
url: "https://github.com/acme/repo/issues/42",
|
||||
createdAt: "2025-01-02T03:04:05Z",
|
||||
} as any);
|
||||
const ghClient = new GitHubClient({ forceMode: "gh-cli" });
|
||||
|
||||
const issue = await ghClient.createIssue({ owner: "acme", repo: "repo", title: "T", body: "B" });
|
||||
|
||||
expect(issue).toEqual({
|
||||
owner: "acme",
|
||||
repo: "repo",
|
||||
number: 42,
|
||||
htmlUrl: "https://github.com/acme/repo/issues/42",
|
||||
createdAt: "2025-01-02T03:04:05Z",
|
||||
});
|
||||
expect(mockRunGhAsync).toHaveBeenCalledWith([
|
||||
"issue",
|
||||
"create",
|
||||
"--repo",
|
||||
"acme/repo",
|
||||
"--title",
|
||||
"T",
|
||||
"--body",
|
||||
"B",
|
||||
]);
|
||||
expect(mockRunGhAsync).toHaveBeenCalledWith(expect.not.arrayContaining(["--json"]));
|
||||
expect(mockRunGhJsonAsync).toHaveBeenCalledWith([
|
||||
"issue",
|
||||
"view",
|
||||
"https://github.com/acme/repo/issues/42",
|
||||
"--json",
|
||||
"number,url,createdAt",
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes labels without adding --json to issue create", async () => {
|
||||
mockRunGhAsync.mockResolvedValue("https://github.com/acme/repo/issues/42\n");
|
||||
mockRunGhJsonAsync.mockResolvedValue({
|
||||
number: 42,
|
||||
url: "https://github.com/acme/repo/issues/42",
|
||||
createdAt: "2025-01-02T03:04:05Z",
|
||||
} as any);
|
||||
const ghClient = new GitHubClient({ forceMode: "gh-cli" });
|
||||
|
||||
await ghClient.createIssue({ owner: "acme", repo: "repo", title: "T", body: "B", labels: ["a", "b"] });
|
||||
|
||||
expect(mockRunGhAsync).toHaveBeenCalledWith([
|
||||
"issue",
|
||||
"create",
|
||||
"--repo",
|
||||
"acme/repo",
|
||||
"--title",
|
||||
"T",
|
||||
"--body",
|
||||
"B",
|
||||
"--label",
|
||||
"a,b",
|
||||
]);
|
||||
expect(mockRunGhAsync).toHaveBeenCalledWith(expect.not.arrayContaining(["--json"]));
|
||||
});
|
||||
|
||||
it("rejects malformed gh stdout without calling issue view", async () => {
|
||||
mockRunGhAsync.mockResolvedValue("oops");
|
||||
const ghClient = new GitHubClient({ forceMode: "gh-cli" });
|
||||
|
||||
await expect(ghClient.createIssue({ owner: "acme", repo: "repo", title: "T", body: "B" })).rejects.toThrow(
|
||||
"Failed to parse issue URL from gh output",
|
||||
);
|
||||
expect(mockRunGhJsonAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPr", () => {
|
||||
const mockPrParams: CreatePrParams = {
|
||||
owner: "test-owner",
|
||||
|
||||
Reference in New Issue
Block a user