translate-app.test.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. })
  74. test("maps product locale codes to their glossaries", () => {
  75. expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
  76. expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
  77. expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
  78. })
  79. test("finds key and placeholder drift", () => {
  80. expect(
  81. findDrift(
  82. { keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
  83. { keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
  84. ),
  85. ).toEqual({ missing: ["missing"], extra: ["extra"], placeholders: ["changed"] })
  86. })
  87. test("accepts locale-specific CLDR plural variants", () => {
  88. expect(
  89. findDrift(
  90. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  91. {
  92. "files.one": "{{count}} ملف",
  93. "files.two": "ملفان: {{count}}",
  94. "files.few": "{{count}} ملفات",
  95. "files.many": "{{count}} ملفًا",
  96. "files.zero": "{{count}} ملف",
  97. "files.other": "{{count}} ملف",
  98. },
  99. "ar",
  100. ),
  101. ).toEqual({ missing: [], extra: [], placeholders: [] })
  102. })
  103. test("reports missing locale-specific CLDR plural variants", () => {
  104. const drift = findDrift(
  105. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  106. { "files.one": "{{count}} ملف", "files.other": "{{count}} ملف" },
  107. "ar",
  108. )
  109. expect(drift.missing).toContain("files.few")
  110. })
  111. test("reports placeholder drift in locale-specific CLDR plural variants", () => {
  112. const drift = findDrift(
  113. { "files.one": "{{count}} file", "files.other": "{{count}} files" },
  114. {
  115. "files.one": "{{count}} ملف",
  116. "files.two": "ملفان: {{count}}",
  117. "files.few": "{{count}} ملفات",
  118. "files.many": "ملفات كثيرة",
  119. "files.zero": "{{count}} ملف",
  120. "files.other": "{{count}} ملف",
  121. },
  122. "ar",
  123. )
  124. expect(drift.placeholders).toContain("files.many")
  125. })
  126. test("runs work with the requested maximum concurrency", async () => {
  127. const active = new Set<number>()
  128. const peaks: number[] = []
  129. const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
  130. active.add(item)
  131. peaks.push(active.size)
  132. await Bun.sleep(5)
  133. active.delete(item)
  134. return item * 2
  135. })
  136. expect(result).toEqual([2, 4, 6, 8, 10])
  137. expect(Math.max(...peaks)).toBe(2)
  138. })
  139. test("reads the actual model and variant from the completed session", () => {
  140. expect(sessionIDFromEvents('shared: https://example.test\n{"type":"step_start","sessionID":"ses_test"}\n')).toBe(
  141. "ses_test",
  142. )
  143. expect(
  144. sessionModels({
  145. messages: [
  146. { info: { role: "user" } },
  147. {
  148. info: {
  149. role: "assistant",
  150. providerID: "opencode",
  151. modelID: "gpt-5.5",
  152. variant: "xhigh",
  153. },
  154. },
  155. ],
  156. }),
  157. ).toEqual([{ model: "opencode/gpt-5.5", variant: "xhigh" }])
  158. expect(
  159. textFromEvents(
  160. 'shared: https://example.test\n{"type":"text","sessionID":"ses_test","part":{"text":"finished"}}\n',
  161. ),
  162. ).toBe("finished")
  163. })
  164. test("resolves variants from verbose model output", () => {
  165. const output = `opencode/other
  166. {"variants":{}}
  167. opencode/gpt-5.5
  168. {"variants":{"high":{"reasoningEffort":"high"},"xhigh":{"reasoningEffort":"xhigh"}}}
  169. opencode/next
  170. {"variants":{}}
  171. `
  172. expect(modelVariants(output, "opencode/gpt-5.5")).toEqual({
  173. high: { reasoningEffort: "high" },
  174. xhigh: { reasoningEffort: "xhigh" },
  175. })
  176. })
  177. test("disables side effects and scopes edits for the translation agent", () => {
  178. const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
  179. expect(config.share).toBe("disabled")
  180. expect(config.formatter).toBe(false)
  181. expect(config.lsp).toBe(false)
  182. expect(config.agent["translate-app-fr"].permission.webfetch).toBe("allow")
  183. expect(config.agent["translate-app-fr"].permission.websearch).toBe("allow")
  184. expect(config.agent["translate-app-fr"].permission.edit).toEqual({
  185. "*": "deny",
  186. "packages/app/src/i18n/fr.ts": "allow",
  187. })
  188. })
  189. test("detects edits outside the locale targets", () => {
  190. expect(
  191. unexpectedChanges(
  192. { "script/translate-app.ts": "before" },
  193. {
  194. "script/translate-app.ts": "before",
  195. "packages/app/src/i18n/fr.ts": "translated",
  196. "packages/app/src/app.tsx": "unexpected",
  197. },
  198. ["packages/app/src/i18n/fr.ts"],
  199. ),
  200. ).toEqual(["packages/app/src/app.tsx"])
  201. expect(unexpectedChanges({ "already-dirty.ts": "before" }, { "already-dirty.ts": "after" }, [])).toEqual([
  202. "already-dirty.ts",
  203. ])
  204. })
  205. })