line-number-validation.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /**
  2. * Line-Number Validation Tests
  3. *
  4. * Comprehensive tests for line-number precision format validation.
  5. * Tests cover all valid and invalid formats, edge cases, and error messages.
  6. */
  7. import { describe, it, expect } from "vitest";
  8. import {
  9. validateLineNumberFormat,
  10. parseLineNumberFormat,
  11. formatLineNumberRanges,
  12. isLineInRanges,
  13. extractLines,
  14. getValidationMessage,
  15. type LineNumberValidationResult,
  16. type ParsedLineRange
  17. } from "../scripts/validators/line-number-validator";
  18. describe("Line-Number Format Validation", () => {
  19. // ==========================================================================
  20. // Valid Format Tests
  21. // ==========================================================================
  22. describe("Valid formats", () => {
  23. it("validates single range format", () => {
  24. const result = validateLineNumberFormat("12-18");
  25. expect(result.valid).toBe(true);
  26. expect(result.errors).toHaveLength(0);
  27. expect(result.parsed).toEqual([{ start: 12, end: 18 }]);
  28. });
  29. it("validates multiple ranges format", () => {
  30. const result = validateLineNumberFormat("12,15-20,25");
  31. expect(result.valid).toBe(true);
  32. expect(result.errors).toHaveLength(0);
  33. expect(result.parsed).toEqual([
  34. { start: 12, end: 12 },
  35. { start: 15, end: 20 },
  36. { start: 25, end: 25 }
  37. ]);
  38. });
  39. it("validates complex multi-range format", () => {
  40. const result = validateLineNumberFormat("1-10,25,30-50,100-120");
  41. expect(result.valid).toBe(true);
  42. expect(result.errors).toHaveLength(0);
  43. expect(result.parsed).toEqual([
  44. { start: 1, end: 10 },
  45. { start: 25, end: 25 },
  46. { start: 30, end: 50 },
  47. { start: 100, end: 120 }
  48. ]);
  49. });
  50. it("validates single line number", () => {
  51. const result = validateLineNumberFormat("42");
  52. expect(result.valid).toBe(true);
  53. expect(result.errors).toHaveLength(0);
  54. expect(result.parsed).toEqual([{ start: 42, end: 42 }]);
  55. });
  56. it("validates multiple single lines", () => {
  57. const result = validateLineNumberFormat("5,10,15,20");
  58. expect(result.valid).toBe(true);
  59. expect(result.errors).toHaveLength(0);
  60. expect(result.parsed).toEqual([
  61. { start: 5, end: 5 },
  62. { start: 10, end: 10 },
  63. { start: 15, end: 15 },
  64. { start: 20, end: 20 }
  65. ]);
  66. });
  67. it("validates range with same start and end (with warning)", () => {
  68. const result = validateLineNumberFormat("10-10");
  69. expect(result.valid).toBe(true);
  70. expect(result.errors).toHaveLength(0);
  71. expect(result.warnings).toBeDefined();
  72. expect(result.warnings![0]).toContain("same start and end");
  73. expect(result.parsed).toEqual([{ start: 10, end: 10 }]);
  74. });
  75. it("validates empty string (entire file)", () => {
  76. const result = validateLineNumberFormat("");
  77. expect(result.valid).toBe(true);
  78. expect(result.errors).toHaveLength(0);
  79. expect(result.parsed).toEqual([]);
  80. });
  81. it("validates undefined (entire file)", () => {
  82. const result = validateLineNumberFormat(undefined);
  83. expect(result.valid).toBe(true);
  84. expect(result.errors).toHaveLength(0);
  85. expect(result.parsed).toEqual([]);
  86. });
  87. it("validates whitespace-only string (entire file)", () => {
  88. const result = validateLineNumberFormat(" ");
  89. expect(result.valid).toBe(true);
  90. expect(result.errors).toHaveLength(0);
  91. expect(result.parsed).toEqual([]);
  92. });
  93. it("validates large line numbers", () => {
  94. const result = validateLineNumberFormat("1000-5000");
  95. expect(result.valid).toBe(true);
  96. expect(result.errors).toHaveLength(0);
  97. expect(result.parsed).toEqual([{ start: 1000, end: 5000 }]);
  98. });
  99. });
  100. // ==========================================================================
  101. // Invalid Format Tests
  102. // ==========================================================================
  103. describe("Invalid formats", () => {
  104. it("rejects reversed range (start > end)", () => {
  105. const result = validateLineNumberFormat("50-10");
  106. expect(result.valid).toBe(false);
  107. expect(result.errors[0]).toContain("start > end");
  108. });
  109. it("rejects invalid separator (colon)", () => {
  110. const result = validateLineNumberFormat("12:18");
  111. expect(result.valid).toBe(false);
  112. expect(result.errors[0]).toContain("Invalid characters");
  113. });
  114. it("rejects invalid separator (double dot)", () => {
  115. const result = validateLineNumberFormat("12..18");
  116. expect(result.valid).toBe(false);
  117. expect(result.errors[0]).toContain("Invalid characters");
  118. });
  119. it("rejects non-numeric values", () => {
  120. const result = validateLineNumberFormat("abc-def");
  121. expect(result.valid).toBe(false);
  122. expect(result.errors[0]).toContain("Invalid characters");
  123. });
  124. it("rejects negative numbers", () => {
  125. const result = validateLineNumberFormat("-5-10");
  126. expect(result.valid).toBe(false);
  127. expect(result.errors[0]).toContain("cannot start or end with a hyphen");
  128. });
  129. it("rejects zero as line number", () => {
  130. const result = validateLineNumberFormat("0-10");
  131. expect(result.valid).toBe(false);
  132. expect(result.errors[0]).toContain("must be positive");
  133. });
  134. it("rejects leading comma", () => {
  135. const result = validateLineNumberFormat(",10-20");
  136. expect(result.valid).toBe(false);
  137. expect(result.errors[0]).toContain("cannot start or end with a comma");
  138. });
  139. it("rejects trailing comma", () => {
  140. const result = validateLineNumberFormat("10-20,");
  141. expect(result.valid).toBe(false);
  142. expect(result.errors[0]).toContain("cannot start or end with a comma");
  143. });
  144. it("rejects consecutive commas", () => {
  145. const result = validateLineNumberFormat("10-20,,30-40");
  146. expect(result.valid).toBe(false);
  147. expect(result.errors[0]).toContain("consecutive commas");
  148. });
  149. it("rejects consecutive hyphens", () => {
  150. const result = validateLineNumberFormat("10--20");
  151. expect(result.valid).toBe(false);
  152. expect(result.errors[0]).toContain("consecutive hyphens");
  153. });
  154. it("rejects empty segment", () => {
  155. const result = validateLineNumberFormat("10-20, ,30-40");
  156. expect(result.valid).toBe(false);
  157. expect(result.errors[0]).toContain("Empty segment");
  158. });
  159. it("rejects range with missing start", () => {
  160. const result = validateLineNumberFormat("-20");
  161. expect(result.valid).toBe(false);
  162. expect(result.errors[0]).toContain("cannot start or end with a hyphen");
  163. });
  164. it("rejects range with missing end", () => {
  165. const result = validateLineNumberFormat("10-");
  166. expect(result.valid).toBe(false);
  167. expect(result.errors[0]).toContain("cannot start or end with a hyphen");
  168. });
  169. it("rejects range with too many hyphens", () => {
  170. const result = validateLineNumberFormat("10-20-30");
  171. expect(result.valid).toBe(false);
  172. expect(result.errors[0]).toContain("Invalid range format");
  173. });
  174. it("rejects mixed valid and invalid ranges", () => {
  175. const result = validateLineNumberFormat("10-20,abc,30-40");
  176. expect(result.valid).toBe(false);
  177. expect(result.errors[0]).toContain("Invalid characters");
  178. });
  179. });
  180. // ==========================================================================
  181. // Warning Tests
  182. // ==========================================================================
  183. describe("Warnings", () => {
  184. it("warns about overlapping ranges", () => {
  185. const result = validateLineNumberFormat("10-20,15-25");
  186. expect(result.valid).toBe(true);
  187. expect(result.warnings).toBeDefined();
  188. expect(result.warnings![0]).toContain("Overlapping ranges");
  189. });
  190. it("warns about adjacent ranges", () => {
  191. const result = validateLineNumberFormat("10-20,20-30");
  192. expect(result.valid).toBe(true);
  193. expect(result.warnings).toBeDefined();
  194. expect(result.warnings![0]).toContain("Overlapping ranges");
  195. });
  196. it("does not warn about non-overlapping ranges", () => {
  197. const result = validateLineNumberFormat("10-20,30-40");
  198. expect(result.valid).toBe(true);
  199. expect(result.warnings).toBeUndefined();
  200. });
  201. it("warns about same start and end in range", () => {
  202. const result = validateLineNumberFormat("10-10");
  203. expect(result.valid).toBe(true);
  204. expect(result.warnings).toBeDefined();
  205. expect(result.warnings![0]).toContain("same start and end");
  206. });
  207. });
  208. // ==========================================================================
  209. // Parser Tests
  210. // ==========================================================================
  211. describe("parseLineNumberFormat", () => {
  212. it("parses valid format into ranges", () => {
  213. const ranges = parseLineNumberFormat("1-10,25,30-50");
  214. expect(ranges).toEqual([
  215. { start: 1, end: 10 },
  216. { start: 25, end: 25 },
  217. { start: 30, end: 50 }
  218. ]);
  219. });
  220. it("returns null for invalid format", () => {
  221. const ranges = parseLineNumberFormat("invalid");
  222. expect(ranges).toBeNull();
  223. });
  224. it("returns empty array for undefined", () => {
  225. const ranges = parseLineNumberFormat(undefined);
  226. expect(ranges).toEqual([]);
  227. });
  228. it("returns empty array for empty string", () => {
  229. const ranges = parseLineNumberFormat("");
  230. expect(ranges).toEqual([]);
  231. });
  232. });
  233. // ==========================================================================
  234. // Formatter Tests
  235. // ==========================================================================
  236. describe("formatLineNumberRanges", () => {
  237. it("formats single range", () => {
  238. const formatted = formatLineNumberRanges([{ start: 12, end: 18 }]);
  239. expect(formatted).toBe("12-18");
  240. });
  241. it("formats multiple ranges", () => {
  242. const formatted = formatLineNumberRanges([
  243. { start: 1, end: 10 },
  244. { start: 25, end: 25 },
  245. { start: 30, end: 50 }
  246. ]);
  247. expect(formatted).toBe("1-10,25,30-50");
  248. });
  249. it("formats single line as number (not range)", () => {
  250. const formatted = formatLineNumberRanges([{ start: 42, end: 42 }]);
  251. expect(formatted).toBe("42");
  252. });
  253. it("formats empty array as empty string", () => {
  254. const formatted = formatLineNumberRanges([]);
  255. expect(formatted).toBe("");
  256. });
  257. });
  258. // ==========================================================================
  259. // Line Inclusion Tests
  260. // ==========================================================================
  261. describe("isLineInRanges", () => {
  262. const ranges: ParsedLineRange[] = [
  263. { start: 1, end: 10 },
  264. { start: 25, end: 25 },
  265. { start: 30, end: 50 }
  266. ];
  267. it("returns true for line in first range", () => {
  268. expect(isLineInRanges(5, ranges)).toBe(true);
  269. });
  270. it("returns true for line at range start", () => {
  271. expect(isLineInRanges(1, ranges)).toBe(true);
  272. });
  273. it("returns true for line at range end", () => {
  274. expect(isLineInRanges(10, ranges)).toBe(true);
  275. });
  276. it("returns true for single line range", () => {
  277. expect(isLineInRanges(25, ranges)).toBe(true);
  278. });
  279. it("returns true for line in last range", () => {
  280. expect(isLineInRanges(40, ranges)).toBe(true);
  281. });
  282. it("returns false for line before all ranges", () => {
  283. expect(isLineInRanges(0, ranges)).toBe(false);
  284. });
  285. it("returns false for line between ranges", () => {
  286. expect(isLineInRanges(15, ranges)).toBe(false);
  287. });
  288. it("returns false for line after all ranges", () => {
  289. expect(isLineInRanges(100, ranges)).toBe(false);
  290. });
  291. it("returns false for empty ranges", () => {
  292. expect(isLineInRanges(10, [])).toBe(false);
  293. });
  294. });
  295. // ==========================================================================
  296. // Line Extraction Tests
  297. // ==========================================================================
  298. describe("extractLines", () => {
  299. const content = `Line 1
  300. Line 2
  301. Line 3
  302. Line 4
  303. Line 5
  304. Line 6
  305. Line 7
  306. Line 8
  307. Line 9
  308. Line 10`;
  309. it("extracts single range", () => {
  310. const extracted = extractLines(content, "3-5");
  311. expect(extracted).toBe("Line 3\nLine 4\nLine 5");
  312. });
  313. it("extracts multiple ranges", () => {
  314. const extracted = extractLines(content, "1-2,5,8-10");
  315. expect(extracted).toBe("Line 1\nLine 2\nLine 5\nLine 8\nLine 9\nLine 10");
  316. });
  317. it("extracts single line", () => {
  318. const extracted = extractLines(content, "5");
  319. expect(extracted).toBe("Line 5");
  320. });
  321. it("returns entire content for undefined lines", () => {
  322. const extracted = extractLines(content, undefined);
  323. expect(extracted).toBe(content);
  324. });
  325. it("returns entire content for empty string", () => {
  326. const extracted = extractLines(content, "");
  327. expect(extracted).toBe(content);
  328. });
  329. it("returns null for invalid format", () => {
  330. const extracted = extractLines(content, "invalid");
  331. expect(extracted).toBeNull();
  332. });
  333. it("handles ranges beyond file length", () => {
  334. const extracted = extractLines(content, "1-100");
  335. expect(extracted).toBe(content);
  336. });
  337. it("handles empty content", () => {
  338. const extracted = extractLines("", "1-10");
  339. expect(extracted).toBe("");
  340. });
  341. });
  342. // ==========================================================================
  343. // Validation Message Tests
  344. // ==========================================================================
  345. describe("getValidationMessage", () => {
  346. it("returns 'Valid' for valid format", () => {
  347. const message = getValidationMessage("10-20");
  348. expect(message).toBe("Valid");
  349. });
  350. it("returns 'Valid (with warnings)' for valid format with warnings", () => {
  351. const message = getValidationMessage("10-10");
  352. expect(message).toContain("Valid (with warnings)");
  353. expect(message).toContain("same start and end");
  354. });
  355. it("returns 'Invalid' with errors for invalid format", () => {
  356. const message = getValidationMessage("50-10");
  357. expect(message).toContain("Invalid:");
  358. expect(message).toContain("start > end");
  359. });
  360. it("returns 'Valid' for undefined", () => {
  361. const message = getValidationMessage(undefined);
  362. expect(message).toBe("Valid");
  363. });
  364. });
  365. // ==========================================================================
  366. // Edge Cases
  367. // ==========================================================================
  368. describe("Edge cases", () => {
  369. it("handles very large line numbers", () => {
  370. const result = validateLineNumberFormat("999999-1000000");
  371. expect(result.valid).toBe(true);
  372. expect(result.parsed).toEqual([{ start: 999999, end: 1000000 }]);
  373. });
  374. it("handles many ranges", () => {
  375. const ranges = Array.from({ length: 100 }, (_, i) => `${i * 10 + 1}-${i * 10 + 5}`).join(',');
  376. const result = validateLineNumberFormat(ranges);
  377. expect(result.valid).toBe(true);
  378. expect(result.parsed).toHaveLength(100);
  379. });
  380. it("handles whitespace in input", () => {
  381. const result = validateLineNumberFormat(" 10-20 ");
  382. expect(result.valid).toBe(true);
  383. expect(result.parsed).toEqual([{ start: 10, end: 20 }]);
  384. });
  385. it("handles single digit line numbers", () => {
  386. const result = validateLineNumberFormat("1-9");
  387. expect(result.valid).toBe(true);
  388. expect(result.parsed).toEqual([{ start: 1, end: 9 }]);
  389. });
  390. it("handles line number 1", () => {
  391. const result = validateLineNumberFormat("1");
  392. expect(result.valid).toBe(true);
  393. expect(result.parsed).toEqual([{ start: 1, end: 1 }]);
  394. });
  395. });
  396. // ==========================================================================
  397. // Real-World Examples
  398. // ==========================================================================
  399. describe("Real-world examples", () => {
  400. it("validates example from enhanced-task-schema.md", () => {
  401. const result = validateLineNumberFormat("53-95");
  402. expect(result.valid).toBe(true);
  403. expect(result.parsed).toEqual([{ start: 53, end: 95 }]);
  404. });
  405. it("validates multi-range example from enhanced-task-schema.md", () => {
  406. const result = validateLineNumberFormat("1-25,120-145,200-220");
  407. expect(result.valid).toBe(true);
  408. expect(result.parsed).toEqual([
  409. { start: 1, end: 25 },
  410. { start: 120, end: 145 },
  411. { start: 200, end: 220 }
  412. ]);
  413. });
  414. it("validates example from test file", () => {
  415. const result = validateLineNumberFormat("10-50");
  416. expect(result.valid).toBe(true);
  417. expect(result.parsed).toEqual([{ start: 10, end: 50 }]);
  418. });
  419. it("validates complex example", () => {
  420. const result = validateLineNumberFormat("1-20,45-60");
  421. expect(result.valid).toBe(true);
  422. expect(result.parsed).toEqual([
  423. { start: 1, end: 20 },
  424. { start: 45, end: 60 }
  425. ]);
  426. });
  427. });
  428. // ==========================================================================
  429. // Integration with Enhanced Schema
  430. // ==========================================================================
  431. describe("Integration with enhanced schema", () => {
  432. it("validates ContextFileReference with valid lines", () => {
  433. const ref = {
  434. path: ".opencode/context/core/standards/code-quality.md",
  435. lines: "53-95",
  436. reason: "Pure function patterns"
  437. };
  438. const result = validateLineNumberFormat(ref.lines);
  439. expect(result.valid).toBe(true);
  440. });
  441. it("validates ContextFileReference without lines (entire file)", () => {
  442. const ref: { path: string; lines?: string; reason: string } = {
  443. path: ".opencode/context/core/standards/code-quality.md",
  444. reason: "All coding standards"
  445. };
  446. const result = validateLineNumberFormat(ref.lines);
  447. expect(result.valid).toBe(true);
  448. });
  449. it("rejects ContextFileReference with invalid lines", () => {
  450. const ref = {
  451. path: ".opencode/context/core/standards/code-quality.md",
  452. lines: "invalid-range",
  453. reason: "Test"
  454. };
  455. const result = validateLineNumberFormat(ref.lines);
  456. expect(result.valid).toBe(false);
  457. });
  458. });
  459. });