feat(FN-4184): defer untitled issues in github-tracking until they receive
Add changeset and implementation to defer untitled GitHub tracking issues, with corresponding tests covering the no-title case. Fusion-Task-Id: FN-4184
This commit is contained in:
5
.changeset/fn-4184-no-untitled-tracking-issue.md
Normal file
5
.changeset/fn-4184-no-untitled-tracking-issue.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
GitHub tracking now waits until a task has a usable title before creating a tracking issue instead of publishing `[FN-XXX] Untitled task` placeholder issues.
|
||||
@@ -264,6 +264,68 @@ describe("maybeCreateTrackingIssue", () => {
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("AI title summarizer failed"));
|
||||
});
|
||||
|
||||
it("returns no_title_available and records activity when no title can be derived", async () => {
|
||||
const recordActivity = vi.fn();
|
||||
const logger = { warn: vi.fn(), info: vi.fn() };
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ title: " ", description: "\n```ts\nconst hidden = true;\n```\n\n ", githubTracking: { enabled: true } }), {
|
||||
taskStore: { recordActivity } as any,
|
||||
projectSettings: {},
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
rootDir,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false, reason: "no_title_available" });
|
||||
expect(createIssueMock).not.toHaveBeenCalled();
|
||||
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "task:updated",
|
||||
details: "GitHub tracking issue not created: task has no title yet",
|
||||
metadata: expect.objectContaining({ type: "github-tracking-no-title" }),
|
||||
}));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("deferred — no usable title"));
|
||||
});
|
||||
|
||||
it("returns no_title_available when the summarizer yields no title and no fallback exists", async () => {
|
||||
const recordActivity = vi.fn();
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({
|
||||
title: "",
|
||||
description: `${" ".repeat(MIN_DESCRIPTION_LENGTH)}\n` + "```md\nignored\n```",
|
||||
githubTracking: { enabled: true },
|
||||
}), {
|
||||
taskStore: { recordActivity, updateTask: vi.fn() } as any,
|
||||
projectSettings: { titleSummarizerProvider: "anthropic", titleSummarizerModelId: "claude" } as any,
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
rootDir,
|
||||
logger: { warn: vi.fn(), info: vi.fn() },
|
||||
});
|
||||
|
||||
expect(summarizeTitleMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({ created: false, reason: "no_title_available" });
|
||||
expect(createIssueMock).not.toHaveBeenCalled();
|
||||
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
|
||||
metadata: expect.objectContaining({ type: "github-tracking-no-title" }),
|
||||
}));
|
||||
});
|
||||
|
||||
it("still creates an issue when the summarizer fails but a description fallback exists", async () => {
|
||||
const logger = { warn: vi.fn(), info: vi.fn() };
|
||||
summarizeTitleMock.mockRejectedValue(new AiServiceError("model unavailable"));
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ title: "", description: longDescription, githubTracking: { enabled: true } }), {
|
||||
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn(), updateTask: vi.fn() } as any,
|
||||
projectSettings: { titleSummarizerProvider: "anthropic", titleSummarizerModelId: "claude" } as any,
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
rootDir,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(result.created).toBe(true);
|
||||
expect(createIssueMock).toHaveBeenCalledWith(expect.objectContaining({ title: "[FN-1] Derived fallback title." }));
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("AI title summarizer failed"));
|
||||
});
|
||||
|
||||
it("does not invoke the summarizer when the description is too short", async () => {
|
||||
await maybeCreateTrackingIssue(buildTask({ title: "", description: "Short title fallback", githubTracking: { enabled: true } }), {
|
||||
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn(), updateTask: vi.fn() } as any,
|
||||
|
||||
@@ -144,6 +144,7 @@ export type MaybeCreateTrackingIssueReason =
|
||||
| "issue_already_linked"
|
||||
| "github_import_source"
|
||||
| "no_repo_configured"
|
||||
| "no_title_available"
|
||||
| "github_error"
|
||||
| "auth_token_missing"
|
||||
| "auth_gh_not_installed"
|
||||
@@ -246,6 +247,21 @@ export async function maybeCreateTrackingIssue(
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveTitle = collapseWhitespace(task.title ?? "")
|
||||
|| deriveTitleFromDescription(task.description, TRACKING_ISSUE_TITLE_LIMIT - `[${task.id}] `.length);
|
||||
|
||||
if (!effectiveTitle) {
|
||||
deps.logger?.info?.(`[github-tracking] ${task.id}: deferred — no usable title; waiting for title or summarizer`);
|
||||
await deps.taskStore.recordActivity({
|
||||
type: "task:updated",
|
||||
taskId: task.id,
|
||||
taskTitle: task.title,
|
||||
details: "GitHub tracking issue not created: task has no title yet",
|
||||
metadata: { type: "github-tracking-no-title" },
|
||||
});
|
||||
return { created: false, reason: "no_title_available" };
|
||||
}
|
||||
|
||||
const resolution = resolveGithubTrackingAuth({
|
||||
projectSettings: deps.projectSettings,
|
||||
globalSettings: deps.globalSettings,
|
||||
|
||||
Reference in New Issue
Block a user