1
0

cli-context-command.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. /**
  2. * `codegraph context` CLI command (#1611).
  3. *
  4. * The usage header has advertised `codegraph context <task> Build context for
  5. * a task` since the first release, and the ContextBuilder behind the public
  6. * `buildContext` API has always shipped in the package — but the command was
  7. * never registered with commander, so external integrations built against the
  8. * documented contract (`codegraph context --path <root> --format json
  9. * --max-nodes 8 --no-code <task>`, e.g. Memorix) got `unknown command
  10. * 'context'` and fell back to their own heuristics.
  11. *
  12. * Exercised end-to-end against the built binary, mirroring
  13. * cli-query-command.test.ts.
  14. */
  15. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  16. import { execFileSync } from 'child_process';
  17. import * as fs from 'fs';
  18. import * as os from 'os';
  19. import * as path from 'path';
  20. import { CodeGraph } from '../src';
  21. const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
  22. const ENV = { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' };
  23. function runContext(cwd: string, extraArgs: string[], taskParts: string[] = ['parseToken', 'expiry', 'handling']): string {
  24. return execFileSync(process.execPath, [BIN, 'context', ...extraArgs, '-p', cwd, ...taskParts], {
  25. encoding: 'utf-8',
  26. env: ENV,
  27. stdio: ['ignore', 'pipe', 'ignore'], // drop stderr (SQLite experimental warning)
  28. });
  29. }
  30. describe('codegraph context — registered CLI command (#1611)', () => {
  31. let tempDir: string;
  32. beforeEach(async () => {
  33. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-cmd-'));
  34. fs.mkdirSync(path.join(tempDir, 'src'));
  35. fs.writeFileSync(
  36. path.join(tempDir, 'src/auth.ts'),
  37. 'export function parseToken(t: string){ return parseTokenExpiry(t) + t.trim().length; }\n' +
  38. 'export function parseTokenExpiry(t: string){ return Date.parse(t); }\n',
  39. );
  40. const cg = CodeGraph.initSync(tempDir);
  41. await cg.indexAll();
  42. cg.close();
  43. });
  44. afterEach(() => {
  45. fs.rmSync(tempDir, { recursive: true, force: true });
  46. });
  47. it('--format json emits clean machine-parseable JSON on stdout', () => {
  48. const parsed = JSON.parse(runContext(tempDir, ['--format', 'json']));
  49. expect(parsed.query).toBe('parseToken expiry handling');
  50. expect(Array.isArray(parsed.nodes)).toBe(true);
  51. expect(parsed.nodes.length).toBeGreaterThan(0);
  52. expect(Array.isArray(parsed.codeBlocks)).toBe(true);
  53. expect(parsed.codeBlocks.length).toBeGreaterThan(0);
  54. });
  55. it('--max-nodes bounds the returned symbol set', () => {
  56. const parsed = JSON.parse(runContext(tempDir, ['--format', 'json', '--max-nodes', '1']));
  57. expect(parsed.nodes.length).toBeLessThanOrEqual(1);
  58. });
  59. it('--no-code omits code blocks (the Memorix contract shape)', () => {
  60. // The exact documented invocation: --format json --max-nodes 8 --no-code
  61. const parsed = JSON.parse(
  62. runContext(tempDir, ['--format', 'json', '--max-nodes', '8', '--no-code']),
  63. );
  64. expect(parsed.codeBlocks).toEqual([]);
  65. expect(parsed.nodes.length).toBeGreaterThan(0);
  66. });
  67. it('defaults to markdown output', () => {
  68. const out = runContext(tempDir, []);
  69. expect(out).toContain('## Code Context');
  70. expect(out).toContain('**Query:** parseToken expiry handling');
  71. });
  72. it('fails cleanly on an uninitialized project', () => {
  73. const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-empty-'));
  74. try {
  75. execFileSync(process.execPath, [BIN, 'context', '-p', empty, 'some', 'task'], {
  76. encoding: 'utf-8',
  77. env: ENV,
  78. stdio: ['ignore', 'pipe', 'pipe'],
  79. });
  80. throw new Error('expected non-zero exit');
  81. } catch (err: any) {
  82. expect(err.status).toBe(1);
  83. expect(String(err.stderr)).toContain('not initialized');
  84. } finally {
  85. fs.rmSync(empty, { recursive: true, force: true });
  86. }
  87. });
  88. it('rejects an unknown --format value', () => {
  89. try {
  90. execFileSync(process.execPath, [BIN, 'context', '--format', 'yaml', '-p', tempDir, 'task'], {
  91. encoding: 'utf-8',
  92. env: ENV,
  93. stdio: ['ignore', 'pipe', 'pipe'],
  94. });
  95. throw new Error('expected non-zero exit');
  96. } catch (err: any) {
  97. expect(err.status).toBe(1);
  98. expect(String(err.stderr)).toContain('Unknown format');
  99. }
  100. });
  101. });