translate-app.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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(["packages/app/src/i18n/tr.ts", "packages/ui/src/i18n/tr.ts"])
  64. })
  65. test("maps product locale codes to their glossaries", () => {
  66. expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
  67. expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
  68. expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
  69. })
  70. test("finds key and placeholder drift", () => {
  71. expect(
  72. findDrift(
  73. { keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
  74. { keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
  75. ),
  76. ).toEqual({ missing: ["missing"], extra: ["extra"], placeholders: ["changed"] })
  77. })
  78. test("runs work with the requested maximum concurrency", async () => {
  79. const active = new Set<number>()
  80. const peaks: number[] = []
  81. const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
  82. active.add(item)
  83. peaks.push(active.size)
  84. await Bun.sleep(5)
  85. active.delete(item)
  86. return item * 2
  87. })
  88. expect(result).toEqual([2, 4, 6, 8, 10])
  89. expect(Math.max(...peaks)).toBe(2)
  90. })
  91. test("reads the actual model and variant from the completed session", () => {
  92. expect(sessionIDFromEvents('shared: https://example.test\n{"type":"step_start","sessionID":"ses_test"}\n')).toBe(
  93. "ses_test",
  94. )
  95. expect(
  96. sessionModels({
  97. messages: [
  98. { info: { role: "user" } },
  99. {
  100. info: {
  101. role: "assistant",
  102. providerID: "opencode",
  103. modelID: "gpt-5.5",
  104. variant: "xhigh",
  105. },
  106. },
  107. ],
  108. }),
  109. ).toEqual([{ model: "opencode/gpt-5.5", variant: "xhigh" }])
  110. expect(
  111. textFromEvents(
  112. 'shared: https://example.test\n{"type":"text","sessionID":"ses_test","part":{"text":"finished"}}\n',
  113. ),
  114. ).toBe("finished")
  115. })
  116. test("resolves variants from verbose model output", () => {
  117. const output = `opencode/other
  118. {"variants":{}}
  119. opencode/gpt-5.5
  120. {"variants":{"high":{"reasoningEffort":"high"},"xhigh":{"reasoningEffort":"xhigh"}}}
  121. opencode/next
  122. {"variants":{}}
  123. `
  124. expect(modelVariants(output, "opencode/gpt-5.5")).toEqual({
  125. high: { reasoningEffort: "high" },
  126. xhigh: { reasoningEffort: "xhigh" },
  127. })
  128. })
  129. test("disables side effects and scopes edits for the translation agent", () => {
  130. const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
  131. expect(config.share).toBe("disabled")
  132. expect(config.formatter).toBe(false)
  133. expect(config.lsp).toBe(false)
  134. expect(config.agent["translate-app-fr"].permission.edit).toEqual({
  135. "*": "deny",
  136. "packages/app/src/i18n/fr.ts": "allow",
  137. })
  138. })
  139. test("detects edits outside the locale targets", () => {
  140. expect(
  141. unexpectedChanges(
  142. { "script/translate-app.ts": "before" },
  143. {
  144. "script/translate-app.ts": "before",
  145. "packages/app/src/i18n/fr.ts": "translated",
  146. "packages/app/src/app.tsx": "unexpected",
  147. },
  148. ["packages/app/src/i18n/fr.ts"],
  149. ),
  150. ).toEqual(["packages/app/src/app.tsx"])
  151. expect(unexpectedChanges({ "already-dirty.ts": "before" }, { "already-dirty.ts": "after" }, [])).toEqual([
  152. "already-dirty.ts",
  153. ])
  154. })
  155. })