status-json.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /**
  2. * Tests for the CI/scripting fields `codegraph status --json` exposes (issue
  3. * #329): the `version`, `indexPath`, and `lastIndexed` fields, plus the
  4. * matching `CodeGraph.getLastIndexedAt()` library method.
  5. *
  6. * The CLI itself is exercised end-to-end against the built binary so the JSON
  7. * field names survive future refactors of the underlying plumbing.
  8. */
  9. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  10. import { execFileSync } from 'child_process';
  11. import * as fs from 'fs';
  12. import * as path from 'path';
  13. import * as os from 'os';
  14. import { CodeGraph } from '../src';
  15. const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
  16. const PKG_VERSION = JSON.parse(
  17. fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'),
  18. ).version as string;
  19. function runStatusJson(cwd: string): Record<string, unknown> {
  20. const stdout = execFileSync(process.execPath, [BIN, 'status', '--json'], {
  21. cwd,
  22. encoding: 'utf-8',
  23. env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
  24. stdio: ['ignore', 'pipe', 'pipe'],
  25. });
  26. // JSON mode prints exactly one line to stdout; be defensive about any stray
  27. // leading output by parsing the last non-empty line.
  28. const line = stdout.trim().split('\n').filter(Boolean).pop()!;
  29. return JSON.parse(line);
  30. }
  31. describe('codegraph status --json — CI fields (#329)', () => {
  32. let tempDir: string;
  33. beforeEach(() => {
  34. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-status-json-'));
  35. });
  36. afterEach(() => {
  37. fs.rmSync(tempDir, { recursive: true, force: true });
  38. });
  39. it('getLastIndexedAt() is null before indexing and a recent ms timestamp after', async () => {
  40. const cg = CodeGraph.initSync(tempDir);
  41. expect(cg.getLastIndexedAt()).toBeNull();
  42. fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
  43. const before = Date.now();
  44. await cg.indexAll();
  45. const after = Date.now();
  46. const last = cg.getLastIndexedAt();
  47. expect(last).not.toBeNull();
  48. expect(typeof last).toBe('number');
  49. expect(last!).toBeGreaterThanOrEqual(before - 1000);
  50. expect(last!).toBeLessThanOrEqual(after + 1000);
  51. cg.close();
  52. });
  53. it('status --json on an UNINITIALIZED project reports version + indexPath + lastIndexed:null', () => {
  54. const out = runStatusJson(tempDir);
  55. expect(out.initialized).toBe(false);
  56. expect(out.version).toBe(PKG_VERSION);
  57. expect(typeof out.indexPath).toBe('string');
  58. expect(out.indexPath as string).toContain('.codegraph');
  59. expect(out.lastIndexed).toBeNull();
  60. });
  61. it('status --json on an INDEXED project reports version + indexPath + a round-trippable lastIndexed', async () => {
  62. fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
  63. const before = Date.now();
  64. const cg = CodeGraph.initSync(tempDir);
  65. await cg.indexAll();
  66. const after = Date.now();
  67. cg.close();
  68. const out = runStatusJson(tempDir);
  69. expect(out.initialized).toBe(true);
  70. expect(out.version).toBe(PKG_VERSION);
  71. expect(out.indexPath as string).toContain('.codegraph');
  72. expect(typeof out.lastIndexed).toBe('string');
  73. // ISO string that round-trips back into the index window.
  74. const ms = Date.parse(out.lastIndexed as string);
  75. expect(ms).toBeGreaterThanOrEqual(before - 1000);
  76. expect(ms).toBeLessThanOrEqual(after + 1000);
  77. });
  78. });
  79. describe('index completeness marker (index_state)', () => {
  80. let tempDir: string;
  81. beforeEach(() => {
  82. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-state-'));
  83. });
  84. afterEach(() => {
  85. fs.rmSync(tempDir, { recursive: true, force: true });
  86. });
  87. it('a clean full index stamps state=complete with reconciled counts', async () => {
  88. fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export function f(): number { return 1; }\n');
  89. fs.writeFileSync(path.join(tempDir, 'b.ts'), 'import { f } from "./a";\nexport const y = f();\n');
  90. const cg = CodeGraph.initSync(tempDir);
  91. const result = await cg.indexAll();
  92. // The scan's ground truth is reported and fully accounted for.
  93. expect(result.filesDiscovered).toBeDefined();
  94. expect(result.filesIndexed + result.filesSkipped + result.filesErrored).toBe(
  95. result.filesDiscovered
  96. );
  97. expect(result.errors.filter((e) => e.code === 'index_partial')).toHaveLength(0);
  98. expect(cg.getIndexState()).toBe('complete');
  99. cg.close();
  100. const out = runStatusJson(tempDir);
  101. expect((out.index as Record<string, unknown>).state).toBe('complete');
  102. });
  103. it('a run killed mid-index leaves state=indexing, and status --json surfaces it', async () => {
  104. fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
  105. const cg = CodeGraph.initSync(tempDir);
  106. await cg.indexAll();
  107. cg.close();
  108. // Simulate a kill between the start-marker write and completion: the
  109. // marker a dead process leaves behind is exactly 'indexing'. Written
  110. // straight into the DB — the process that died can't have cleaned it up.
  111. // (require, not import: vite tries to bundle a dynamic import specifier.)
  112. // eslint-disable-next-line @typescript-eslint/no-require-imports
  113. const { DatabaseSync } = require('node:sqlite');
  114. const db = new DatabaseSync(path.join(tempDir, '.codegraph', 'codegraph.db'));
  115. db.prepare(
  116. "INSERT INTO project_metadata (key, value, updated_at) VALUES ('index_state', 'indexing', 0) " +
  117. "ON CONFLICT(key) DO UPDATE SET value = 'indexing'"
  118. ).run();
  119. db.close();
  120. const out = runStatusJson(tempDir);
  121. expect((out.index as Record<string, unknown>).state).toBe('indexing');
  122. const reopened = await CodeGraph.open(tempDir);
  123. expect(reopened.getIndexState()).toBe('indexing');
  124. reopened.close();
  125. });
  126. });