audit-settings-placeholders-script.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import fs from "node:fs";
  2. import path from "node:path";
  3. import { describe, expect, test } from "vitest";
  4. import audit from "../../../scripts/audit-settings-placeholders.js";
  5. describe("scripts/audit-settings-placeholders.js", () => {
  6. test("findSettingsPlaceholders() reports leaf strings that equal zh-CN at the same key path", () => {
  7. const tmpRoot = path.join(
  8. process.cwd(),
  9. "tests",
  10. ".tmp-audit-settings-placeholders",
  11. String(Date.now())
  12. );
  13. const messagesDir = path.join(tmpRoot, "messages");
  14. const writeJson = (p: string, data: unknown) => {
  15. fs.mkdirSync(path.dirname(p), { recursive: true });
  16. fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
  17. };
  18. try {
  19. // canonical (zh-CN)
  20. writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), {
  21. form: {
  22. enableHttp2: "启用 HTTP/2",
  23. enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。",
  24. },
  25. });
  26. writeJson(
  27. path.join(messagesDir, "zh-CN", "settings", "providers", "form", "maxRetryAttempts.json"),
  28. {
  29. label: "单供应商最大尝试次数",
  30. desc: "包含首次调用在内,单个供应商最多尝试几次后切换。",
  31. placeholder: "2",
  32. }
  33. );
  34. // target (en) has one placeholder leaf copied from zh-CN, and one translated value
  35. writeJson(path.join(messagesDir, "en", "settings", "config.json"), {
  36. form: {
  37. enableHttp2: "Enable HTTP/2",
  38. enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。",
  39. },
  40. });
  41. writeJson(
  42. path.join(messagesDir, "en", "settings", "providers", "form", "maxRetryAttempts.json"),
  43. {
  44. label: "single provider max retry attempts",
  45. desc: "包含首次调用在内,单个供应商最多尝试几次后切换。",
  46. placeholder: "2",
  47. }
  48. );
  49. const report = audit.findSettingsPlaceholders({ messagesDir, locales: ["en"] });
  50. expect(report.rows.length).toBe(2);
  51. const keys = report.rows.map((r: { key: string }) => r.key).sort();
  52. expect(keys).toEqual(["config.form.enableHttp2Desc", "providers.form.maxRetryAttempts.desc"]);
  53. } finally {
  54. fs.rmSync(tmpRoot, { recursive: true, force: true });
  55. }
  56. });
  57. test("CLI prints OK and exits 0 when no placeholders exist", () => {
  58. const tmpRoot = path.join(
  59. process.cwd(),
  60. "tests",
  61. ".tmp-audit-settings-placeholders-cli",
  62. `ok-${Date.now()}`
  63. );
  64. const messagesDir = path.join(tmpRoot, "messages");
  65. const writeJson = (p: string, data: unknown) => {
  66. fs.mkdirSync(path.dirname(p), { recursive: true });
  67. fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
  68. };
  69. try {
  70. writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), {
  71. form: { enableHttp2: "启用 HTTP/2" },
  72. });
  73. writeJson(path.join(messagesDir, "en", "settings", "config.json"), {
  74. form: { enableHttp2: "Enable HTTP/2" },
  75. });
  76. const out = audit.run([`--messagesDir=${messagesDir}`]);
  77. expect(out.exitCode).toBe(0);
  78. expect(out.lines.join("\n")).toContain("OK: no zh-CN placeholder candidates");
  79. } finally {
  80. fs.rmSync(tmpRoot, { recursive: true, force: true });
  81. }
  82. });
  83. test("CLI prints matches and exits 1 when --fail and placeholders exist", () => {
  84. const tmpRoot = path.join(
  85. process.cwd(),
  86. "tests",
  87. ".tmp-audit-settings-placeholders-cli",
  88. `fail-${Date.now()}`
  89. );
  90. const messagesDir = path.join(tmpRoot, "messages");
  91. const writeJson = (p: string, data: unknown) => {
  92. fs.mkdirSync(path.dirname(p), { recursive: true });
  93. fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
  94. };
  95. try {
  96. writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), {
  97. form: { enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。" },
  98. });
  99. writeJson(path.join(messagesDir, "en", "settings", "config.json"), {
  100. form: { enableHttp2Desc: "启用后,代理请求将优先使用 HTTP/2 协议。" },
  101. });
  102. const out = audit.run([`--messagesDir=${messagesDir}`, "--fail"]);
  103. expect(out.exitCode).toBe(1);
  104. expect(out.lines[0]).toBe("Found 1 zh-CN placeholder candidates:");
  105. expect(out.lines).toContain("en\tconfig.json\tconfig.form.enableHttp2Desc");
  106. } finally {
  107. fs.rmSync(tmpRoot, { recursive: true, force: true });
  108. }
  109. });
  110. test("CLI supports --scope and --format=tsv for stable machine parsing", () => {
  111. const tmpRoot = path.join(
  112. process.cwd(),
  113. "tests",
  114. ".tmp-audit-settings-placeholders-cli",
  115. `tsv-${Date.now()}`
  116. );
  117. const messagesDir = path.join(tmpRoot, "messages");
  118. const writeJson = (p: string, data: unknown) => {
  119. fs.mkdirSync(path.dirname(p), { recursive: true });
  120. fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
  121. };
  122. try {
  123. writeJson(path.join(messagesDir, "zh-CN", "dashboard.json"), {
  124. hero: { title: "仪表盘" },
  125. });
  126. writeJson(path.join(messagesDir, "en", "dashboard.json"), {
  127. hero: { title: "仪表盘" },
  128. });
  129. const out = audit.run([
  130. `--messagesDir=${messagesDir}`,
  131. "--scope=dashboard",
  132. "--locales=en",
  133. "--format=tsv",
  134. ]);
  135. expect(out.exitCode).toBe(0);
  136. expect(out.lines[0]).toBe("locale\trelFile\tkey\tvalue\treason");
  137. expect(out.lines).toContain(
  138. "en\tdashboard.json\tdashboard.hero.title\t仪表盘\tsame_as_zh-CN"
  139. );
  140. } finally {
  141. fs.rmSync(tmpRoot, { recursive: true, force: true });
  142. }
  143. });
  144. test("allowlist filters false positives (exact/keyPrefix/keyRegex/valueRegex/glossary)", () => {
  145. const tmpRoot = path.join(
  146. process.cwd(),
  147. "tests",
  148. ".tmp-audit-settings-placeholders-allowlist",
  149. `allowlist-${Date.now()}`
  150. );
  151. const messagesDir = path.join(tmpRoot, "messages");
  152. const allowlistPath = path.join(tmpRoot, "allowlist.json");
  153. const writeJson = (p: string, data: unknown) => {
  154. fs.mkdirSync(path.dirname(p), { recursive: true });
  155. fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
  156. };
  157. try {
  158. writeJson(allowlistPath, {
  159. entries: [
  160. { key: "config.form.exactKey", reason: "test-exact" },
  161. { keyPrefix: "config.form.prefix.", reason: "test-prefix" },
  162. { keyRegex: "^config\\.form\\.re\\.", reason: "test-key-regex" },
  163. { valueRegex: "KEEP_AS_CN$", reason: "test-value-regex" },
  164. ],
  165. glossary: ["GLOSSARY_TERM"],
  166. });
  167. writeJson(path.join(messagesDir, "zh-CN", "settings", "config.json"), {
  168. form: {
  169. exactKey: "精确豁免",
  170. prefix: {
  171. a: "前缀豁免",
  172. },
  173. re: {
  174. b: "正则豁免",
  175. },
  176. value: "触发 KEEP_AS_CN",
  177. glossary: "包含 GLOSSARY_TERM 的句子",
  178. },
  179. });
  180. writeJson(path.join(messagesDir, "en", "settings", "config.json"), {
  181. form: {
  182. exactKey: "精确豁免",
  183. prefix: {
  184. a: "前缀豁免",
  185. },
  186. re: {
  187. b: "正则豁免",
  188. },
  189. value: "触发 KEEP_AS_CN",
  190. glossary: "包含 GLOSSARY_TERM 的句子",
  191. },
  192. });
  193. const report = audit.findSettingsPlaceholders({
  194. messagesDir,
  195. locales: ["en"],
  196. scopes: ["settings"],
  197. allowlistPath,
  198. });
  199. expect(report.rows).toEqual([]);
  200. } finally {
  201. fs.rmSync(tmpRoot, { recursive: true, force: true });
  202. }
  203. });
  204. });