include-ignored-config.test.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * `codegraph.json` `includeIgnored` loader (#970, #976 / #622, #699).
  3. *
  4. * Parsing, validation, and mtime-caching of the opt-in patterns that re-include
  5. * gitignored directories for embedded-repo discovery. The behavioral end of this
  6. * feature (scanDirectory / discoverEmbeddedRepoRoots / sync honoring the patterns)
  7. * lives in `multi-repo-workspace.test.ts`; these are the loader unit tests,
  8. * mirroring the `extensions` loader coverage in `extension-mapping.test.ts`.
  9. *
  10. * Invariant under test: every failure mode degrades to the zero-config default
  11. * (empty patterns → `.gitignore` fully respected), never a throw.
  12. */
  13. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  14. import * as fs from 'node:fs';
  15. import * as path from 'node:path';
  16. import * as os from 'node:os';
  17. import { loadIncludeIgnoredPatterns, loadExtensionOverrides, clearProjectConfigCache, addIncludeIgnoredPatterns } from '../src/project-config';
  18. describe('includeIgnored loader (codegraph.json)', () => {
  19. let dir: string;
  20. beforeEach(() => {
  21. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-includeignored-'));
  22. clearProjectConfigCache();
  23. });
  24. afterEach(() => {
  25. clearProjectConfigCache();
  26. fs.rmSync(dir, { recursive: true, force: true });
  27. });
  28. const writeConfig = (obj: unknown) =>
  29. fs.writeFileSync(
  30. path.join(dir, 'codegraph.json'),
  31. typeof obj === 'string' ? obj : JSON.stringify(obj)
  32. );
  33. it('returns an empty list when there is no codegraph.json (the default)', () => {
  34. expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
  35. });
  36. it('loads a well-formed pattern array', () => {
  37. writeConfig({ includeIgnored: ['packages/', 'services/'] });
  38. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/', 'services/']);
  39. });
  40. it('trims whitespace and drops blank / non-string entries', () => {
  41. writeConfig({ includeIgnored: [' packages/ ', '', ' ', 42, null, 'services/'] });
  42. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/', 'services/']);
  43. });
  44. it('ignores a non-array includeIgnored value without throwing', () => {
  45. writeConfig({ includeIgnored: 'packages/' });
  46. expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
  47. });
  48. it('ignores malformed JSON without throwing', () => {
  49. writeConfig('{ not: valid json ');
  50. expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
  51. });
  52. it('returns [] when the field is absent but other config is present', () => {
  53. writeConfig({ extensions: { '.foo': 'typescript' } });
  54. expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
  55. });
  56. it('coexists with extensions in one file (shared single parse)', () => {
  57. writeConfig({ extensions: { '.foo': 'typescript' }, includeIgnored: ['vendor/'] });
  58. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'typescript' });
  59. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['vendor/']);
  60. });
  61. it('picks up a changed config (mtime-invalidated cache)', () => {
  62. writeConfig({ includeIgnored: ['packages/'] });
  63. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/']);
  64. writeConfig({ includeIgnored: ['services/'] });
  65. // Force a distinct mtime in case the filesystem clock is coarse.
  66. const future = new Date(Date.now() + 2000);
  67. fs.utimesSync(path.join(dir, 'codegraph.json'), future, future);
  68. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['services/']);
  69. });
  70. it('drops the patterns again when the config file is removed', () => {
  71. writeConfig({ includeIgnored: ['packages/'] });
  72. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/']);
  73. fs.rmSync(path.join(dir, 'codegraph.json'));
  74. expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
  75. });
  76. });
  77. describe('addIncludeIgnoredPatterns (codegraph.json writer, #1156)', () => {
  78. let dir: string;
  79. beforeEach(() => {
  80. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-addincludeignored-'));
  81. clearProjectConfigCache();
  82. });
  83. afterEach(() => {
  84. clearProjectConfigCache();
  85. fs.rmSync(dir, { recursive: true, force: true });
  86. });
  87. const readConfig = () => JSON.parse(fs.readFileSync(path.join(dir, 'codegraph.json'), 'utf-8'));
  88. it('creates codegraph.json when none exists', () => {
  89. expect(addIncludeIgnoredPatterns(dir, ['mtc-a/', 'mtc-b/'])).toBe(2);
  90. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['mtc-a/', 'mtc-b/']);
  91. });
  92. it('merges into an existing list, preserving other keys and de-duping', () => {
  93. fs.writeFileSync(
  94. path.join(dir, 'codegraph.json'),
  95. JSON.stringify({ extensions: { '.foo': 'typescript' }, includeIgnored: ['mtc-a/'] }),
  96. );
  97. expect(addIncludeIgnoredPatterns(dir, ['mtc-a/', 'mtc-b/'])).toBe(1); // only mtc-b/ is new
  98. const parsed = readConfig();
  99. expect(parsed.includeIgnored).toEqual(['mtc-a/', 'mtc-b/']);
  100. expect(parsed.extensions).toEqual({ '.foo': 'typescript' }); // untouched
  101. });
  102. it('is idempotent — re-adding the same patterns adds nothing', () => {
  103. addIncludeIgnoredPatterns(dir, ['mtc-a/']);
  104. expect(addIncludeIgnoredPatterns(dir, ['mtc-a/'])).toBe(0);
  105. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['mtc-a/']);
  106. });
  107. it('replaces a non-array includeIgnored value rather than crashing', () => {
  108. fs.writeFileSync(path.join(dir, 'codegraph.json'), JSON.stringify({ includeIgnored: 'oops' }));
  109. expect(addIncludeIgnoredPatterns(dir, ['mtc-a/'])).toBe(1);
  110. expect(loadIncludeIgnoredPatterns(dir)).toEqual(['mtc-a/']);
  111. });
  112. it('refuses to clobber a malformed existing codegraph.json (throws, leaves file intact)', () => {
  113. const bad = '{ not: valid json ';
  114. fs.writeFileSync(path.join(dir, 'codegraph.json'), bad);
  115. expect(() => addIncludeIgnoredPatterns(dir, ['mtc-a/'])).toThrow();
  116. expect(fs.readFileSync(path.join(dir, 'codegraph.json'), 'utf-8')).toBe(bad);
  117. });
  118. it('writes pretty-printed, newline-terminated JSON', () => {
  119. addIncludeIgnoredPatterns(dir, ['mtc-a/']);
  120. const raw = fs.readFileSync(path.join(dir, 'codegraph.json'), 'utf-8');
  121. expect(raw.endsWith('\n')).toBe(true);
  122. expect(raw).toContain('\n "includeIgnored"'); // 2-space indent
  123. });
  124. });