FN-7979: add route regressions for translated GitHub issue import
Lock GitHub import routes so tasks use cached translated title/body with source URL appended, and fail open to original prose when translation is off, missing, or for closed issues. - Mock getImportTranslation on the route test store - Cover single-issue and batch import happy paths for cached translations - Assert fail-open when auto-translate is off, cache misses, or issue is closed - Prefer persistent getSettings mocks so translation settings survive multi-call import flows - Document CLI/GitLab exclusion in FNXC surface note Files changed: .../dashboard/src/__tests__/routes-github.test.ts | 216 ++++++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7979 Fusion-Task-Lineage: 7b0fe0a5-44f3-4131-b535-6b4c7602b5a8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -183,6 +183,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
unarchiveTask: vi.fn(),
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
getSettingsFast: vi.fn().mockResolvedValue({}),
|
||||
getImportTranslation: vi.fn().mockResolvedValue(null),
|
||||
updateSettings: vi.fn(),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
getSettingsByScope: vi.fn().mockResolvedValue({ global: {}, project: {} }),
|
||||
@@ -557,6 +558,10 @@ describe("POST /github/issues/fetch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:GitHubImportTranslate 2026-07-15-10:00:
|
||||
These route regressions lock the operator-facing invariant that GitHub-imported tasks carry cached translated prose, with the source URL appended afterward. CLI and GitLab imports are intentionally excluded: neither is a GitHub dashboard translation surface backed by this durable cache.
|
||||
*/
|
||||
describe("POST /github/issues/import", () => {
|
||||
let store: TaskStore;
|
||||
let getIssueSpy: ReturnType<typeof vi.fn>;
|
||||
@@ -627,7 +632,7 @@ describe("POST /github/issues/import", () => {
|
||||
});
|
||||
|
||||
it("marks a single imported issue as tracked when tracking defaults are on", async () => {
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ githubTrackingEnabledByDefault: true });
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ githubTrackingEnabledByDefault: true });
|
||||
getIssueSpy.mockResolvedValueOnce(mockGitHubIssue);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
@@ -642,7 +647,7 @@ describe("POST /github/issues/import", () => {
|
||||
});
|
||||
|
||||
it("marks a single imported issue as tracked when import linking is on and new-task defaults are off", async () => {
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingEnabledByDefault: false,
|
||||
githubLinkImportedIssuesToTracking: true,
|
||||
});
|
||||
@@ -775,6 +780,114 @@ describe("POST /github/issues/import", () => {
|
||||
expect(store.createTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("imports cached translated prose and appends the source URL", async () => {
|
||||
const translatedTitle = "Translated issue title";
|
||||
const translatedBody = "Translated issue body";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle,
|
||||
translatedBody,
|
||||
detectedLocale: null,
|
||||
});
|
||||
getIssueSpy.mockResolvedValueOnce(mockGitHubIssue);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: translatedTitle.slice(0, 200),
|
||||
description: `${translatedBody}\n\nSource: ${mockGitHubIssue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("uses the global dashboard language for cached translated prose", async () => {
|
||||
const translatedTitle = "Global-language translated title";
|
||||
const translatedBody = "Global-language translated body";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
language: "en",
|
||||
});
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle,
|
||||
translatedBody,
|
||||
detectedLocale: null,
|
||||
});
|
||||
getIssueSpy.mockResolvedValueOnce(mockGitHubIssue);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: translatedTitle,
|
||||
description: `${translatedBody}\n\nSource: ${mockGitHubIssue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose without reading the cache when auto-translate is off", async () => {
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle: "Cached title that must not be used",
|
||||
translatedBody: "Cached body that must not be used",
|
||||
detectedLocale: null,
|
||||
});
|
||||
getIssueSpy.mockResolvedValueOnce(mockGitHubIssue);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.getImportTranslation).not.toHaveBeenCalled();
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: mockGitHubIssue.title,
|
||||
description: `${mockGitHubIssue.body}\n\nSource: ${mockGitHubIssue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose when the translation cache misses", async () => {
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
getIssueSpy.mockResolvedValueOnce(mockGitHubIssue);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: mockGitHubIssue.title,
|
||||
description: `${mockGitHubIssue.body}\n\nSource: ${mockGitHubIssue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose for closed issues", async () => {
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle: "Closed cached title that must not be used",
|
||||
translatedBody: "Closed cached body that must not be used",
|
||||
detectedLocale: null,
|
||||
});
|
||||
const closedIssue = { ...mockGitHubIssue, state: "closed" as const };
|
||||
getIssueSpy.mockResolvedValueOnce(closedIssue);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/import", JSON.stringify({ owner: "owner", repo: "repo", issueNumber: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: closedIssue.title,
|
||||
description: `${closedIssue.body}\n\nSource: ${closedIssue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("truncates long titles to 200 chars", async () => {
|
||||
const longTitleIssue = {
|
||||
...mockGitHubIssue,
|
||||
@@ -853,6 +966,105 @@ describe("POST /github/issues/batch-import", () => {
|
||||
labels: [{ name: "bug" }],
|
||||
});
|
||||
|
||||
it("imports cached translated prose and appends the source URL", async () => {
|
||||
const issue = mockGitHubIssue(1, "Original batch title");
|
||||
const translatedTitle = "Translated batch title";
|
||||
const translatedBody = "Translated batch body";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle,
|
||||
translatedBody,
|
||||
detectedLocale: null,
|
||||
});
|
||||
vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: issue,
|
||||
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/github/issues/batch-import", JSON.stringify({ owner: "owner", repo: "repo", issueNumbers: [1], delayMs: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: translatedTitle.slice(0, 200),
|
||||
description: `${translatedBody}\n\nSource: ${issue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose without reading the cache when auto-translate is off", async () => {
|
||||
const issue = mockGitHubIssue(1, "Original batch title");
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle: "Cached batch title that must not be used",
|
||||
translatedBody: "Cached batch body that must not be used",
|
||||
detectedLocale: null,
|
||||
});
|
||||
vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: issue,
|
||||
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/batch-import", JSON.stringify({ owner: "owner", repo: "repo", issueNumbers: [1], delayMs: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.getImportTranslation).not.toHaveBeenCalled();
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: issue.title,
|
||||
description: `${issue.body}\n\nSource: ${issue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose when the translation cache misses", async () => {
|
||||
const issue = mockGitHubIssue(1, "Original batch title");
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: issue,
|
||||
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/batch-import", JSON.stringify({ owner: "owner", repo: "repo", issueNumbers: [1], delayMs: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: issue.title,
|
||||
description: `${issue.body}\n\nSource: ${issue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails open to original prose for closed issues", async () => {
|
||||
const issue = { ...mockGitHubIssue(1, "Closed batch title"), state: "closed" as const };
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubImportAutoTranslate: true,
|
||||
importTranslateTargetLocale: "en",
|
||||
});
|
||||
(store.getImportTranslation as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
translatedTitle: "Closed cached batch title that must not be used",
|
||||
translatedBody: "Closed cached batch body that must not be used",
|
||||
detectedLocale: null,
|
||||
});
|
||||
vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: issue,
|
||||
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
|
||||
|
||||
await REQUEST(buildApp(), "POST", "/api/github/issues/batch-import", JSON.stringify({ owner: "owner", repo: "repo", issueNumbers: [1], delayMs: 1 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: issue.title,
|
||||
description: `${issue.body}\n\nSource: ${issue.html_url}`,
|
||||
}));
|
||||
});
|
||||
|
||||
it("imports multiple issues successfully", async () => {
|
||||
const throttledSpy = vi.spyOn(GitHubClient.prototype, "fetchThrottled")
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
Reference in New Issue
Block a user