diff --git a/packages/dashboard/src/__tests__/routes-github.test.ts b/packages/dashboard/src/__tests__/routes-github.test.ts index 49d51276ee..dd021cfc60 100644 --- a/packages/dashboard/src/__tests__/routes-github.test.ts +++ b/packages/dashboard/src/__tests__/routes-github.test.ts @@ -51,6 +51,8 @@ const { mockExecFile, mockGetCachedImportTranslation, mockResolveTargetLocale, + mockTranslateText, + mockCheckTranslateRateLimit, } = vi.hoisted(() => ({ mockPerformUpdateCheck: vi.fn(), mockClearUpdateCheckCache: vi.fn(), @@ -58,8 +60,19 @@ const { mockExecFile: vi.fn(), mockGetCachedImportTranslation: vi.fn(), mockResolveTargetLocale: vi.fn(), + mockTranslateText: vi.fn(), + mockCheckTranslateRateLimit: vi.fn(), })); +vi.mock("../ai-translate.js", async () => { + const actual = await vi.importActual("../ai-translate.js"); + return { + ...actual, + translateText: mockTranslateText, + checkTranslateRateLimit: mockCheckTranslateRateLimit, + }; +}); + vi.mock("../update-check.js", async () => { const actual = await vi.importActual("../update-check.js"); return { @@ -587,6 +600,73 @@ describe("POST /github/issues/fetch", () => { }); }); +/* +FNXC:GitHubImportTranslate 2026-07-20-00:00: +FN-8417 requires the HTTP reopen boundary, not just the translation service, to +prove that a second identical auto-translate request uses the durable cache. +The route must reserve rate-limit budget only for uncached model work. +*/ +describe("POST /github/issues/auto-translate", () => { + let store: TaskStore; + + beforeEach(() => { + const translations = new Map(); + store = createMockStore({ + getSettings: vi.fn().mockResolvedValue({ + githubImportAutoTranslate: true, + importTranslateTargetLocale: "en", + }), + getImportTranslation: vi.fn(async (key) => translations.get(JSON.stringify(key)) ?? null), + recordImportTranslation: vi.fn(async (key, value) => { + translations.set(JSON.stringify(key), { + ...value, + detectedLocale: value.detectedLocale ?? null, + recordedAt: "2026-07-20T00:00:00.000Z", + }); + }), + }); + mockTranslateText.mockResolvedValue({ title: "Translated title", body: "Translated body" }); + mockCheckTranslateRateLimit.mockReturnValue(true); + }); + + function buildApp() { + const app = express(); + app.use(express.json()); + app.use("/api", createApiRoutes(store)); + return app; + } + + it("auto-translate serves a second identical POST from cache without model or rate-limit cost", async () => { + const body = JSON.stringify({ + owner: "owner", + repo: "repo", + targetLocale: "en", + items: [{ number: 17, title: "Error del servidor", body: "El servidor devuelve un error cuando el usuario intenta guardar los cambios en la configuracion. No se puede completar la operacion porque el sistema no responde. Por favor revise los registros del servidor para mas informacion sobre este problema.", state: "open" }], + }); + + const first = await REQUEST(buildApp(), "POST", "/api/github/issues/auto-translate", body, { + "Content-Type": "application/json", + }); + const second = await REQUEST(buildApp(), "POST", "/api/github/issues/auto-translate", body, { + "Content-Type": "application/json", + }); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + expect(mockTranslateText).toHaveBeenCalledTimes(1); + expect(first.body.translations[17]).toEqual({ title: "Translated title", body: "Translated body" }); + expect(second.body.translations[17]).toEqual({ title: "Translated title", body: "Translated body" }); + expect(mockCheckTranslateRateLimit).toHaveBeenCalledTimes(1); + expect(mockCheckTranslateRateLimit).toHaveBeenCalledWith(expect.any(String), 1); + expect(store.recordImportTranslation).toHaveBeenCalledTimes(1); + }); +}); + describe("POST /github/issues/import", () => { let store: TaskStore; let getIssueSpy: ReturnType;