pi-lens 2.2.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Format Service Tests
3
+ *
4
+ * Tests concurrent formatter execution via Effect-TS
5
+ * and FileTime integration for safety.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
12
+ import { FileTimeError } from "../file-time.js";
13
+ import { FormatService, getFormatService, resetFormatService, } from "../format-service.js";
14
+ import { clearAllSessions } from "../file-time.js";
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const TEST_DIR = path.join(__dirname, "..", "..", "test-format-service");
18
+ describe("FormatService", () => {
19
+ let formatService;
20
+ const sessionID = "test-format-session";
21
+ beforeEach(() => {
22
+ resetFormatService();
23
+ clearAllSessions(); // Clear FileTime global state for test isolation
24
+ formatService = new FormatService(sessionID, true);
25
+ if (fs.existsSync(TEST_DIR)) {
26
+ fs.rmSync(TEST_DIR, { recursive: true });
27
+ }
28
+ fs.mkdirSync(TEST_DIR, { recursive: true });
29
+ });
30
+ afterEach(() => {
31
+ resetFormatService();
32
+ clearAllSessions(); // Clear FileTime global state for test isolation
33
+ if (fs.existsSync(TEST_DIR)) {
34
+ fs.rmSync(TEST_DIR, { recursive: true });
35
+ }
36
+ });
37
+ describe("formatFile()", () => {
38
+ it("should skip formatting when disabled", async () => {
39
+ const disabledService = new FormatService(sessionID, false);
40
+ const testFile = path.join(TEST_DIR, "disabled.txt");
41
+ fs.writeFileSync(testFile, "content");
42
+ const result = await disabledService.formatFile(testFile);
43
+ expect(result.formatters).toEqual([]);
44
+ expect(result.anyChanged).toBe(false);
45
+ expect(result.allSucceeded).toBe(true);
46
+ });
47
+ it("should skip formatting with skip option", async () => {
48
+ const testFile = path.join(TEST_DIR, "skipped.txt");
49
+ fs.writeFileSync(testFile, "content");
50
+ const result = await formatService.formatFile(testFile, { skip: true });
51
+ expect(result.formatters).toEqual([]);
52
+ expect(result.anyChanged).toBe(false);
53
+ expect(result.allSucceeded).toBe(true);
54
+ });
55
+ it("should skip when file modified externally", async () => {
56
+ const testFile = path.join(TEST_DIR, "external.txt");
57
+ fs.writeFileSync(testFile, "original");
58
+ // Record read
59
+ formatService.recordRead(testFile);
60
+ // Modify externally
61
+ fs.writeFileSync(testFile, "modified");
62
+ const result = await formatService.formatFile(testFile);
63
+ expect(result.formatters).toEqual([]);
64
+ expect(result.anyChanged).toBe(false);
65
+ expect(result.allSucceeded).toBe(false);
66
+ });
67
+ it("should format TypeScript file with biome config", async () => {
68
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), '{"formatter": {}}');
69
+ const tsFile = path.join(TEST_DIR, "test.ts");
70
+ fs.writeFileSync(tsFile, "const x=1;");
71
+ // Record read so format service knows initial state
72
+ formatService.recordRead(tsFile);
73
+ const result = await formatService.formatFile(tsFile);
74
+ expect(result.filePath).toBe(tsFile);
75
+ expect(result.formatters.some((f) => f.name === "biome")).toBe(true);
76
+ });
77
+ it("should format Python file with ruff config", async () => {
78
+ fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.ruff]\nline-length = 100");
79
+ const pyFile = path.join(TEST_DIR, "test.py");
80
+ fs.writeFileSync(pyFile, "x=1");
81
+ // Record read so format service knows initial state
82
+ formatService.recordRead(pyFile);
83
+ const result = await formatService.formatFile(pyFile);
84
+ expect(result.filePath).toBe(pyFile);
85
+ expect(result.formatters.some((f) => f.name === "ruff")).toBe(true);
86
+ });
87
+ it("should run multiple formatters for same file", async () => {
88
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
89
+ fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }));
90
+ const tsFile = path.join(TEST_DIR, "test.ts");
91
+ fs.writeFileSync(tsFile, "const x = 1;");
92
+ formatService.recordRead(tsFile);
93
+ const result = await formatService.formatFile(tsFile);
94
+ const names = result.formatters.map((f) => f.name);
95
+ expect(names).toContain("biome");
96
+ });
97
+ it("should return empty result for unsupported file", async () => {
98
+ const txtFile = path.join(TEST_DIR, "test.txt");
99
+ fs.writeFileSync(txtFile, "content");
100
+ formatService.recordRead(txtFile);
101
+ const result = await formatService.formatFile(txtFile);
102
+ expect(result.formatters).toEqual([]);
103
+ expect(result.anyChanged).toBe(false);
104
+ expect(result.allSucceeded).toBe(true);
105
+ });
106
+ it("should record FileTime after formatting", async () => {
107
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
108
+ const tsFile = path.join(TEST_DIR, "test.ts");
109
+ fs.writeFileSync(tsFile, "const x = 1;");
110
+ formatService.recordRead(tsFile);
111
+ await formatService.formatFile(tsFile);
112
+ expect(() => formatService.assertUnchanged(tsFile)).not.toThrow();
113
+ });
114
+ it("should report success/failure for each formatter", async () => {
115
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
116
+ const tsFile = path.join(TEST_DIR, "test.ts");
117
+ fs.writeFileSync(tsFile, "const x = 1;");
118
+ formatService.recordRead(tsFile);
119
+ const result = await formatService.formatFile(tsFile);
120
+ for (const formatter of result.formatters) {
121
+ expect(formatter).toHaveProperty("name");
122
+ expect(formatter).toHaveProperty("success");
123
+ expect(formatter).toHaveProperty("changed");
124
+ expect(typeof formatter.success).toBe("boolean");
125
+ expect(typeof formatter.changed).toBe("boolean");
126
+ }
127
+ });
128
+ it("should re-read file content after formatting", async () => {
129
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
130
+ const tsFile = path.join(TEST_DIR, "test.ts");
131
+ fs.writeFileSync(tsFile, "const x = 1;");
132
+ formatService.recordRead(tsFile);
133
+ const _result = await formatService.formatFile(tsFile);
134
+ const after = fs.readFileSync(tsFile, "utf-8");
135
+ expect(after).toBeDefined();
136
+ });
137
+ });
138
+ describe("assertUnchanged()", () => {
139
+ it("should not throw for unchanged files", async () => {
140
+ const testFile = path.join(TEST_DIR, "unchanged.txt");
141
+ fs.writeFileSync(testFile, "content");
142
+ formatService.recordRead(testFile);
143
+ expect(() => formatService.assertUnchanged(testFile)).not.toThrow();
144
+ });
145
+ it("should throw FileTimeError when file modified", async () => {
146
+ const testFile = path.join(TEST_DIR, "modified.txt");
147
+ fs.writeFileSync(testFile, "original");
148
+ formatService.recordRead(testFile);
149
+ fs.writeFileSync(testFile, "changed");
150
+ expect(() => formatService.assertUnchanged(testFile)).toThrow(FileTimeError);
151
+ });
152
+ });
153
+ describe("hasChanged()", () => {
154
+ it("should return false for unchanged files", async () => {
155
+ const testFile = path.join(TEST_DIR, "unchanged-check.txt");
156
+ fs.writeFileSync(testFile, "content");
157
+ formatService.recordRead(testFile);
158
+ expect(formatService.hasChanged(testFile)).toBe(false);
159
+ });
160
+ it("should return true when file modified", async () => {
161
+ const testFile = path.join(TEST_DIR, "changed-check.txt");
162
+ fs.writeFileSync(testFile, "original");
163
+ formatService.recordRead(testFile);
164
+ // Small delay to ensure different mtime (Windows has ~16ms resolution)
165
+ await new Promise((r) => setTimeout(r, 50));
166
+ fs.writeFileSync(testFile, "modified");
167
+ expect(formatService.hasChanged(testFile)).toBe(true);
168
+ });
169
+ it("should return true for unread files", async () => {
170
+ const testFile = path.join(TEST_DIR, "never-read.txt");
171
+ fs.writeFileSync(testFile, "content");
172
+ expect(formatService.hasChanged(testFile)).toBe(true);
173
+ });
174
+ });
175
+ describe("recordRead()", () => {
176
+ it("should record file read for tracking", async () => {
177
+ const testFile = path.join(TEST_DIR, "tracked.txt");
178
+ fs.writeFileSync(testFile, "content");
179
+ formatService.recordRead(testFile);
180
+ expect(formatService.hasChanged(testFile)).toBe(false);
181
+ });
182
+ });
183
+ describe("clearCache()", () => {
184
+ it("should clear formatter detection cache", async () => {
185
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
186
+ const tsFile = path.join(TEST_DIR, "test.ts");
187
+ fs.writeFileSync(tsFile, "const x = 1;");
188
+ formatService.recordRead(tsFile);
189
+ await formatService.formatFile(tsFile);
190
+ formatService.clearCache();
191
+ await formatService.formatFile(tsFile);
192
+ });
193
+ });
194
+ describe("Concurrency", () => {
195
+ it("should handle multiple files concurrently", async () => {
196
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
197
+ const files = [
198
+ path.join(TEST_DIR, "file1.ts"),
199
+ path.join(TEST_DIR, "file2.ts"),
200
+ path.join(TEST_DIR, "file3.ts"),
201
+ ];
202
+ for (const file of files) {
203
+ fs.writeFileSync(file, "const x = 1;");
204
+ formatService.recordRead(file);
205
+ }
206
+ const results = await Promise.all(files.map((f) => formatService.formatFile(f)));
207
+ expect(results).toHaveLength(3);
208
+ for (const result of results) {
209
+ expect(result.filePath).toBeDefined();
210
+ expect(result.formatters.length).toBeGreaterThan(0);
211
+ }
212
+ });
213
+ });
214
+ });
215
+ describe("getFormatService singleton", () => {
216
+ beforeEach(() => {
217
+ resetFormatService();
218
+ });
219
+ afterEach(() => {
220
+ resetFormatService();
221
+ });
222
+ it("should return singleton instance", () => {
223
+ const instance1 = getFormatService();
224
+ const instance2 = getFormatService();
225
+ expect(instance1).toBe(instance2);
226
+ });
227
+ it("should create new instance when session ID provided", () => {
228
+ const instance1 = getFormatService("session1");
229
+ const instance2 = getFormatService("session2");
230
+ expect(instance1).not.toBe(instance2);
231
+ });
232
+ it("should use cached instance regardless of enabled flag changes", () => {
233
+ const first = getFormatService("test", true);
234
+ const second = getFormatService("test", false);
235
+ expect(first).toBe(second);
236
+ });
237
+ });
238
+ describe("resetFormatService", () => {
239
+ it("should reset singleton instance", () => {
240
+ const instance1 = getFormatService();
241
+ resetFormatService();
242
+ const instance2 = getFormatService();
243
+ expect(instance1).not.toBe(instance2);
244
+ });
245
+ });
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Format Service Tests
3
+ *
4
+ * Tests concurrent formatter execution via Effect-TS
5
+ * and FileTime integration for safety.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
+ import { FileTimeError } from "../file-time.js";
14
+ import {
15
+ FormatService,
16
+ getFormatService,
17
+ resetFormatService,
18
+ } from "../format-service.js";
19
+ import { clearAllSessions } from "../file-time.js";
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ const TEST_DIR = path.join(__dirname, "..", "..", "test-format-service");
25
+
26
+ describe("FormatService", () => {
27
+ let formatService: FormatService;
28
+ const sessionID = "test-format-session";
29
+
30
+ beforeEach(() => {
31
+ resetFormatService();
32
+ clearAllSessions(); // Clear FileTime global state for test isolation
33
+ formatService = new FormatService(sessionID, true);
34
+
35
+ if (fs.existsSync(TEST_DIR)) {
36
+ fs.rmSync(TEST_DIR, { recursive: true });
37
+ }
38
+ fs.mkdirSync(TEST_DIR, { recursive: true });
39
+ });
40
+
41
+ afterEach(() => {
42
+ resetFormatService();
43
+ clearAllSessions(); // Clear FileTime global state for test isolation
44
+ if (fs.existsSync(TEST_DIR)) {
45
+ fs.rmSync(TEST_DIR, { recursive: true });
46
+ }
47
+ });
48
+
49
+ describe("formatFile()", () => {
50
+ it("should skip formatting when disabled", async () => {
51
+ const disabledService = new FormatService(sessionID, false);
52
+ const testFile = path.join(TEST_DIR, "disabled.txt");
53
+ fs.writeFileSync(testFile, "content");
54
+
55
+ const result = await disabledService.formatFile(testFile);
56
+
57
+ expect(result.formatters).toEqual([]);
58
+ expect(result.anyChanged).toBe(false);
59
+ expect(result.allSucceeded).toBe(true);
60
+ });
61
+
62
+ it("should skip formatting with skip option", async () => {
63
+ const testFile = path.join(TEST_DIR, "skipped.txt");
64
+ fs.writeFileSync(testFile, "content");
65
+
66
+ const result = await formatService.formatFile(testFile, { skip: true });
67
+
68
+ expect(result.formatters).toEqual([]);
69
+ expect(result.anyChanged).toBe(false);
70
+ expect(result.allSucceeded).toBe(true);
71
+ });
72
+
73
+ it("should skip when file modified externally", async () => {
74
+ const testFile = path.join(TEST_DIR, "external.txt");
75
+ fs.writeFileSync(testFile, "original");
76
+
77
+ // Record read
78
+ formatService.recordRead(testFile);
79
+
80
+ // Modify externally
81
+ fs.writeFileSync(testFile, "modified");
82
+
83
+ const result = await formatService.formatFile(testFile);
84
+
85
+ expect(result.formatters).toEqual([]);
86
+ expect(result.anyChanged).toBe(false);
87
+ expect(result.allSucceeded).toBe(false);
88
+ });
89
+
90
+ it("should format TypeScript file with biome config", async () => {
91
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), '{"formatter": {}}');
92
+ const tsFile = path.join(TEST_DIR, "test.ts");
93
+ fs.writeFileSync(tsFile, "const x=1;");
94
+
95
+ // Record read so format service knows initial state
96
+ formatService.recordRead(tsFile);
97
+
98
+ const result = await formatService.formatFile(tsFile);
99
+
100
+ expect(result.filePath).toBe(tsFile);
101
+ expect(result.formatters.some((f) => f.name === "biome")).toBe(true);
102
+ });
103
+
104
+ it("should format Python file with ruff config", async () => {
105
+ fs.writeFileSync(
106
+ path.join(TEST_DIR, "pyproject.toml"),
107
+ "[tool.ruff]\nline-length = 100",
108
+ );
109
+ const pyFile = path.join(TEST_DIR, "test.py");
110
+ fs.writeFileSync(pyFile, "x=1");
111
+
112
+ // Record read so format service knows initial state
113
+ formatService.recordRead(pyFile);
114
+
115
+ const result = await formatService.formatFile(pyFile);
116
+
117
+ expect(result.filePath).toBe(pyFile);
118
+ expect(result.formatters.some((f) => f.name === "ruff")).toBe(true);
119
+ });
120
+
121
+ it("should run multiple formatters for same file", async () => {
122
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
123
+ fs.writeFileSync(
124
+ path.join(TEST_DIR, "package.json"),
125
+ JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }),
126
+ );
127
+ const tsFile = path.join(TEST_DIR, "test.ts");
128
+ fs.writeFileSync(tsFile, "const x = 1;");
129
+
130
+ formatService.recordRead(tsFile);
131
+
132
+ const result = await formatService.formatFile(tsFile);
133
+
134
+ const names = result.formatters.map((f) => f.name);
135
+ expect(names).toContain("biome");
136
+ });
137
+
138
+ it("should return empty result for unsupported file", async () => {
139
+ const txtFile = path.join(TEST_DIR, "test.txt");
140
+ fs.writeFileSync(txtFile, "content");
141
+
142
+ formatService.recordRead(txtFile);
143
+
144
+ const result = await formatService.formatFile(txtFile);
145
+
146
+ expect(result.formatters).toEqual([]);
147
+ expect(result.anyChanged).toBe(false);
148
+ expect(result.allSucceeded).toBe(true);
149
+ });
150
+
151
+ it("should record FileTime after formatting", async () => {
152
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
153
+ const tsFile = path.join(TEST_DIR, "test.ts");
154
+ fs.writeFileSync(tsFile, "const x = 1;");
155
+
156
+ formatService.recordRead(tsFile);
157
+
158
+ await formatService.formatFile(tsFile);
159
+
160
+ expect(() => formatService.assertUnchanged(tsFile)).not.toThrow();
161
+ });
162
+
163
+ it("should report success/failure for each formatter", async () => {
164
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
165
+ const tsFile = path.join(TEST_DIR, "test.ts");
166
+ fs.writeFileSync(tsFile, "const x = 1;");
167
+
168
+ formatService.recordRead(tsFile);
169
+
170
+ const result = await formatService.formatFile(tsFile);
171
+
172
+ for (const formatter of result.formatters) {
173
+ expect(formatter).toHaveProperty("name");
174
+ expect(formatter).toHaveProperty("success");
175
+ expect(formatter).toHaveProperty("changed");
176
+ expect(typeof formatter.success).toBe("boolean");
177
+ expect(typeof formatter.changed).toBe("boolean");
178
+ }
179
+ });
180
+
181
+ it("should re-read file content after formatting", async () => {
182
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
183
+ const tsFile = path.join(TEST_DIR, "test.ts");
184
+ fs.writeFileSync(tsFile, "const x = 1;");
185
+
186
+ formatService.recordRead(tsFile);
187
+
188
+ const _result = await formatService.formatFile(tsFile);
189
+
190
+ const after = fs.readFileSync(tsFile, "utf-8");
191
+ expect(after).toBeDefined();
192
+ });
193
+ });
194
+
195
+ describe("assertUnchanged()", () => {
196
+ it("should not throw for unchanged files", async () => {
197
+ const testFile = path.join(TEST_DIR, "unchanged.txt");
198
+ fs.writeFileSync(testFile, "content");
199
+ formatService.recordRead(testFile);
200
+
201
+ expect(() => formatService.assertUnchanged(testFile)).not.toThrow();
202
+ });
203
+
204
+ it("should throw FileTimeError when file modified", async () => {
205
+ const testFile = path.join(TEST_DIR, "modified.txt");
206
+ fs.writeFileSync(testFile, "original");
207
+ formatService.recordRead(testFile);
208
+
209
+ fs.writeFileSync(testFile, "changed");
210
+
211
+ expect(() => formatService.assertUnchanged(testFile)).toThrow(
212
+ FileTimeError,
213
+ );
214
+ });
215
+ });
216
+
217
+ describe("hasChanged()", () => {
218
+ it("should return false for unchanged files", async () => {
219
+ const testFile = path.join(TEST_DIR, "unchanged-check.txt");
220
+ fs.writeFileSync(testFile, "content");
221
+ formatService.recordRead(testFile);
222
+
223
+ expect(formatService.hasChanged(testFile)).toBe(false);
224
+ });
225
+
226
+ it("should return true when file modified", async () => {
227
+ const testFile = path.join(TEST_DIR, "changed-check.txt");
228
+ fs.writeFileSync(testFile, "original");
229
+ formatService.recordRead(testFile);
230
+
231
+ // Small delay to ensure different mtime (Windows has ~16ms resolution)
232
+ await new Promise((r) => setTimeout(r, 50));
233
+ fs.writeFileSync(testFile, "modified");
234
+
235
+ expect(formatService.hasChanged(testFile)).toBe(true);
236
+ });
237
+
238
+ it("should return true for unread files", async () => {
239
+ const testFile = path.join(TEST_DIR, "never-read.txt");
240
+ fs.writeFileSync(testFile, "content");
241
+
242
+ expect(formatService.hasChanged(testFile)).toBe(true);
243
+ });
244
+ });
245
+
246
+ describe("recordRead()", () => {
247
+ it("should record file read for tracking", async () => {
248
+ const testFile = path.join(TEST_DIR, "tracked.txt");
249
+ fs.writeFileSync(testFile, "content");
250
+
251
+ formatService.recordRead(testFile);
252
+
253
+ expect(formatService.hasChanged(testFile)).toBe(false);
254
+ });
255
+ });
256
+
257
+ describe("clearCache()", () => {
258
+ it("should clear formatter detection cache", async () => {
259
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
260
+ const tsFile = path.join(TEST_DIR, "test.ts");
261
+ fs.writeFileSync(tsFile, "const x = 1;");
262
+ formatService.recordRead(tsFile);
263
+
264
+ await formatService.formatFile(tsFile);
265
+
266
+ formatService.clearCache();
267
+
268
+ await formatService.formatFile(tsFile);
269
+ });
270
+ });
271
+
272
+ describe("Concurrency", () => {
273
+ it("should handle multiple files concurrently", async () => {
274
+ fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
275
+
276
+ const files = [
277
+ path.join(TEST_DIR, "file1.ts"),
278
+ path.join(TEST_DIR, "file2.ts"),
279
+ path.join(TEST_DIR, "file3.ts"),
280
+ ];
281
+
282
+ for (const file of files) {
283
+ fs.writeFileSync(file, "const x = 1;");
284
+ formatService.recordRead(file);
285
+ }
286
+
287
+ const results = await Promise.all(
288
+ files.map((f) => formatService.formatFile(f)),
289
+ );
290
+
291
+ expect(results).toHaveLength(3);
292
+ for (const result of results) {
293
+ expect(result.filePath).toBeDefined();
294
+ expect(result.formatters.length).toBeGreaterThan(0);
295
+ }
296
+ });
297
+ });
298
+ });
299
+
300
+ describe("getFormatService singleton", () => {
301
+ beforeEach(() => {
302
+ resetFormatService();
303
+ });
304
+
305
+ afterEach(() => {
306
+ resetFormatService();
307
+ });
308
+
309
+ it("should return singleton instance", () => {
310
+ const instance1 = getFormatService();
311
+ const instance2 = getFormatService();
312
+
313
+ expect(instance1).toBe(instance2);
314
+ });
315
+
316
+ it("should create new instance when session ID provided", () => {
317
+ const instance1 = getFormatService("session1");
318
+ const instance2 = getFormatService("session2");
319
+
320
+ expect(instance1).not.toBe(instance2);
321
+ });
322
+
323
+ it("should use cached instance regardless of enabled flag changes", () => {
324
+ const first = getFormatService("test", true);
325
+ const second = getFormatService("test", false);
326
+
327
+ expect(first).toBe(second);
328
+ });
329
+ });
330
+
331
+ describe("resetFormatService", () => {
332
+ it("should reset singleton instance", () => {
333
+ const instance1 = getFormatService();
334
+ resetFormatService();
335
+ const instance2 = getFormatService();
336
+
337
+ expect(instance1).not.toBe(instance2);
338
+ });
339
+ });