1
0

translate-app.test.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. findDrift,
  4. glossaryFile,
  5. modelVariants,
  6. parseTranslationArgs,
  7. runPool,
  8. sessionIDFromEvents,
  9. sessionModels,
  10. targetFiles,
  11. textFromEvents,
  12. translationConfig,
  13. unexpectedChanges,
  14. } from "./translate-app"
  15. describe("translate app", () => {
  16. test("parses one locale with the public model defaults", () => {
  17. expect(parseTranslationArgs(["fr"])).toEqual({
  18. target: "fr",
  19. concurrency: 1,
  20. model: "opencode/gpt-5.5",
  21. variant: "xhigh",
  22. dryRun: false,
  23. check: false,
  24. help: false,
  25. })
  26. })
  27. test("parses all locales with bounded concurrency overrides", () => {
  28. expect(
  29. parseTranslationArgs([
  30. "all",
  31. "--concurrency",
  32. "7",
  33. "--model",
  34. "opencode/gpt-5.4",
  35. "--variant",
  36. "high",
  37. "--dry-run",
  38. ]),
  39. ).toEqual({
  40. target: "all",
  41. concurrency: 7,
  42. model: "opencode/gpt-5.4",
  43. variant: "high",
  44. dryRun: true,
  45. check: false,
  46. help: false,
  47. })
  48. })
  49. test("rejects unsupported targets and invalid concurrency", () => {
  50. expect(() => parseTranslationArgs(["en"])).toThrow("Unknown locale")
  51. expect(() => parseTranslationArgs(["fr", "de"])).toThrow("one locale")
  52. expect(() => parseTranslationArgs(["all", "--concurrency", "0"])).toThrow("positive integer")
  53. })
  54. test("parses fresh-process parity checks without requesting translation", () => {
  55. expect(parseTranslationArgs(["fr", "--check"]).check).toBe(true)
  56. })
  57. test("limits each locale to its app surfaces", () => {
  58. expect(targetFiles("fr")).toEqual([
  59. "packages/app/src/i18n/fr.ts",
  60. "packages/ui/src/i18n/fr.ts",
  61. "packages/desktop/src/renderer/i18n/fr.ts",
  62. ])
  63. expect(targetFiles("tr")).toEqual([
  64. "packages/app/src/i18n/tr.ts",
  65. "packages/ui/src/i18n/tr.ts",
  66. "packages/desktop/src/renderer/i18n/tr.ts",
  67. ])
  68. expect(targetFiles("dv")).toEqual([
  69. "packages/app/src/i18n/dv.ts",
  70. "packages/ui/src/i18n/dv.ts",
  71. "packages/desktop/src/renderer/i18n/dv.ts",
  72. ])
  73. expect(targetFiles("he")).toEqual([
  74. "packages/app/src/i18n/he.ts",
  75. "packages/ui/src/i18n/he.ts",
  76. "packages/desktop/src/renderer/i18n/he.ts",
  77. ])
  78. })
  79. test("maps product locale codes to their glossaries", () => {
  80. expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
  81. expect(glossaryFile("he")).toBe(".opencode/glossary/he.md")
  82. expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
  83. expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
  84. })
  85. test("finds key and placeholder drift", () => {
  86. expect(
  87. findDrift(
  88. { keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
  89. { keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
  90. ),
  91. ).toEqual({ missing: ["missing"], extra: ["extra"], placeholders: ["changed"] })
  92. })
  93. test("accepts locale-specific CLDR plural variants", () => {
  94. expect(
  95. findDrift(
  96. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  97. {
  98. "files.one": "{{count}} ملف",
  99. "files.two": "ملفان: {{count}}",
  100. "files.few": "{{count}} ملفات",
  101. "files.many": "{{count}} ملفًا",
  102. "files.zero": "{{count}} ملف",
  103. "files.other": "{{count}} ملف",
  104. },
  105. "ar",
  106. ),
  107. ).toEqual({ missing: [], extra: [], placeholders: [] })
  108. })
  109. test("reports missing locale-specific CLDR plural variants", () => {
  110. const drift = findDrift(
  111. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  112. { "files.one": "{{count}} ملف", "files.other": "{{count}} ملف" },
  113. "ar",
  114. )
  115. expect(drift.missing).toContain("files.few")
  116. })
  117. test("reports placeholder drift in locale-specific CLDR plural variants", () => {
  118. const drift = findDrift(
  119. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  120. {
  121. "files.one": "{{count}} ملف",
  122. "files.two": "ملفان: {{count}}",
  123. "files.few": "{{count}} ملفات",
  124. "files.many": "ملفات كثيرة",
  125. "files.zero": "{{count}} ملف",
  126. "files.other": "{{count}} ملف",
  127. },
  128. "ar",
  129. )
  130. expect(drift.placeholders).toContain("files.many")
  131. })
  132. test("runs work with the requested maximum concurrency", async () => {
  133. const active = new Set<number>()
  134. const peaks: number[] = []
  135. const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
  136. active.add(item)
  137. peaks.push(active.size)
  138. await Bun.sleep(5)
  139. active.delete(item)
  140. return item * 2
  141. })
  142. expect(result).toEqual([2, 4, 6, 8, 10])
  143. expect(Math.max(...peaks)).toBe(2)
  144. })
  145. test("reads the actual model and variant from the completed session", () => {
  146. expect(sessionIDFromEvents('shared: https://example.test\n{"type":"step_start","sessionID":"ses_test"}\n')).toBe(
  147. "ses_test",
  148. )
  149. expect(
  150. sessionModels({
  151. messages: [
  152. { info: { role: "user" } },
  153. {
  154. info: {
  155. role: "assistant",
  156. providerID: "opencode",
  157. modelID: "gpt-5.5",
  158. variant: "xhigh",
  159. },
  160. },
  161. ],
  162. }),
  163. ).toEqual([{ model: "opencode/gpt-5.5", variant: "xhigh" }])
  164. expect(
  165. textFromEvents(
  166. 'shared: https://example.test\n{"type":"text","sessionID":"ses_test","part":{"text":"finished"}}\n',
  167. ),
  168. ).toBe("finished")
  169. })
  170. test("resolves variants from verbose model output", () => {
  171. const output = `opencode/other
  172. {"variants":{}}
  173. opencode/gpt-5.5
  174. {"variants":{"high":{"reasoningEffort":"high"},"xhigh":{"reasoningEffort":"xhigh"}}}
  175. opencode/next
  176. {"variants":{}}
  177. `
  178. expect(modelVariants(output, "opencode/gpt-5.5")).toEqual({
  179. high: { reasoningEffort: "high" },
  180. xhigh: { reasoningEffort: "xhigh" },
  181. })
  182. })
  183. test("disables side effects and scopes edits for the translation agent", () => {
  184. const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
  185. expect(config.share).toBe("disabled")
  186. expect(config.formatter).toBe(false)
  187. expect(config.lsp).toBe(false)
  188. expect(config.agent["translate-app-fr"].permission.webfetch).toBe("allow")
  189. expect(config.agent["translate-app-fr"].permission.websearch).toBe("allow")
  190. expect(config.agent["translate-app-fr"].permission.edit).toEqual({
  191. "*": "deny",
  192. "packages/app/src/i18n/fr.ts": "allow",
  193. })
  194. })
  195. test("detects edits outside the locale targets", () => {
  196. expect(
  197. unexpectedChanges(
  198. { "script/translate-app.ts": "before" },
  199. {
  200. "script/translate-app.ts": "before",
  201. "packages/app/src/i18n/fr.ts": "translated",
  202. "packages/app/src/app.tsx": "unexpected",
  203. },
  204. ["packages/app/src/i18n/fr.ts"],
  205. ),
  206. ).toEqual(["packages/app/src/app.tsx"])
  207. expect(unexpectedChanges({ "already-dirty.ts": "before" }, { "already-dirty.ts": "after" }, [])).toEqual([
  208. "already-dirty.ts",
  209. ])
  210. })
  211. })