diff --git a/server/src/__tests__/multilingual-issues-routes.test.ts b/server/src/__tests__/multilingual-issues-routes.test.ts index 9ef0501a5f4..da9f0aa3d0c 100644 --- a/server/src/__tests__/multilingual-issues-routes.test.ts +++ b/server/src/__tests__/multilingual-issues-routes.test.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import express from "express"; import request from "supertest"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { companies, createDb } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, @@ -9,9 +9,10 @@ import { } from "./helpers/embedded-postgres.js"; import { errorHandler } from "../middleware/index.js"; import { issueRoutes } from "../routes/issues.js"; +import type { StorageService } from "../storage/types.js"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); -const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe.sequential : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( @@ -22,16 +23,65 @@ if (!embeddedPostgresSupport.supported) { describeEmbeddedPostgres("multilingual issue routes", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; + let app!: ReturnType; + let companyId!: string; + + const title = "验证中文任务"; + const description = [ + "请用中文回复并保留上下文。", + "日本語: 次の手順を書いてください。", + "हिन्दी: कृपया स्थिति बताएं।", + ].join("\n"); + const firstReply = [ + "结果: 中文响应保留。", + "日本語の返信も保持。", + "हिन्दी उत्तर भी सुरक्षित है।", + ].join("\n"); + const completionNote = [ + "完成: 已验证中文。", + "日本語: 完了しました。", + "हिन्दी: सत्यापन पूरा हुआ।", + ].join("\n"); + const documentBody = [ + "# QA notes", + "", + "- 中文: 可以创建、读取、搜索、评论。", + "- 日本語: ドキュメント本文を保持します。", + "- हिन्दी: दस्तावेज़ पाठ सुरक्षित रहता है।", + ].join("\n"); beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-multilingual-issues-"); db = createDb(tempDb.connectionString); + companyId = randomUUID(); + app = createApp(companyId); + + await db.insert(companies).values({ + id: companyId, + name: "Multilingual tenant", + issuePrefix: "LNG", + requireBoardApprovalForNewAgents: false, + }); }, 20_000); afterAll(async () => { await tempDb?.cleanup(); }); + function createStorage(): StorageService { + return { + provider: "local_disk", + putFile: vi.fn(async () => { + throw new Error("Unexpected storage.putFile call in multilingual issue route test"); + }), + getObject: vi.fn(async () => { + throw new Error("Unexpected storage.getObject call in multilingual issue route test"); + }), + headObject: vi.fn(async () => ({ exists: false })), + deleteObject: vi.fn(async () => undefined), + }; + } + function createApp(companyId: string) { const app = express(); app.use(express.json()); @@ -46,45 +96,12 @@ describeEmbeddedPostgres("multilingual issue routes", () => { }; next(); }); - app.use("/api", issueRoutes(db, {} as any)); + app.use("/api", issueRoutes(db, createStorage())); app.use(errorHandler); return app; } - it("round-trips Chinese, Japanese, and Hindi issue work through create, search, comments, documents, and completion", async () => { - const companyId = randomUUID(); - const title = "验证中文任务"; - const description = [ - "请用中文回复并保留上下文。", - "日本語: 次の手順を書いてください。", - "हिन्दी: कृपया स्थिति बताएं।", - ].join("\n"); - const firstReply = [ - "结果: 中文响应保留。", - "日本語の返信も保持。", - "हिन्दी उत्तर भी सुरक्षित है।", - ].join("\n"); - const completionNote = [ - "完成: 已验证中文。", - "日本語: 完了しました。", - "हिन्दी: सत्यापन पूरा हुआ।", - ].join("\n"); - const documentBody = [ - "# QA notes", - "", - "- 中文: 可以创建、读取、搜索、评论。", - "- 日本語: ドキュメント本文を保持します。", - "- हिन्दी: दस्तावेज़ पाठ सुरक्षित रहता है।", - ].join("\n"); - - await db.insert(companies).values({ - id: companyId, - name: "Multilingual tenant", - issuePrefix: "LNG", - requireBoardApprovalForNewAgents: false, - }); - - const app = createApp(companyId); + it("creates an issue with multilingual title and description", async () => { const createRes = await request(app) .post(`/api/companies/${companyId}/issues`) .send({ @@ -102,22 +119,30 @@ describeEmbeddedPostgres("multilingual issue routes", () => { priority: "medium", identifier: "LNG-1", }); + }); + it("reads the multilingual title and description unchanged", async () => { const getRes = await request(app).get("/api/issues/LNG-1"); expect(getRes.status, JSON.stringify(getRes.body)).toBe(200); expect(getRes.body.title).toBe(title); expect(getRes.body.description).toBe(description); + }); + it("finds the issue by Chinese search text", async () => { const searchRes = await request(app).get(`/api/companies/${companyId}/issues`).query({ q: "中文" }); expect(searchRes.status, JSON.stringify(searchRes.body)).toBe(200); expect(searchRes.body.map((issue: { identifier: string }) => issue.identifier)).toContain("LNG-1"); + }); + it("preserves multilingual comment bodies", async () => { const commentRes = await request(app) .post("/api/issues/LNG-1/comments") .send({ body: firstReply }); expect(commentRes.status, JSON.stringify(commentRes.body)).toBe(201); expect(commentRes.body.body).toBe(firstReply); + }); + it("preserves multilingual document bodies", async () => { const documentRes = await request(app) .put("/api/issues/LNG-1/documents/qa-notes") .send({ @@ -127,21 +152,27 @@ describeEmbeddedPostgres("multilingual issue routes", () => { }); expect(documentRes.status, JSON.stringify(documentRes.body)).toBe(201); expect(documentRes.body.body).toBe(documentBody); + }); + it("preserves multilingual completion comments", async () => { const completeRes = await request(app) .patch("/api/issues/LNG-1") .send({ status: "done", comment: completionNote }); expect(completeRes.status, JSON.stringify(completeRes.body)).toBe(200); expect(completeRes.body.status).toBe("done"); expect(completeRes.body.comment.body).toBe(completionNote); + }); + it("lists multilingual comments in write order", async () => { const commentsRes = await request(app).get("/api/issues/LNG-1/comments").query({ order: "asc" }); expect(commentsRes.status, JSON.stringify(commentsRes.body)).toBe(200); expect(commentsRes.body.map((comment: { body: string }) => comment.body)).toEqual([ firstReply, completionNote, ]); + }); + it("exposes multilingual issue text in heartbeat context", async () => { const heartbeatContextRes = await request(app).get("/api/issues/LNG-1/heartbeat-context"); expect(heartbeatContextRes.status, JSON.stringify(heartbeatContextRes.body)).toBe(200); expect(heartbeatContextRes.body.issue.title).toBe(title);