cli-no-color.test.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /**
  2. * Color handling for CLI output (issue #1281).
  3. *
  4. * Contract:
  5. * - piped/redirected stdout (the spawned-child default here) -> no ANSI codes
  6. * - NO_COLOR set and non-empty -> no ANSI codes (https://no-color.org)
  7. * - --no-color anywhere on the command line -> no ANSI codes, even vs FORCE_COLOR
  8. * - FORCE_COLOR / --color -> ANSI codes even when piped
  9. *
  10. * Exercised end-to-end against the built binary so every list command's output
  11. * path is covered by the same switch.
  12. */
  13. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  14. import { execFileSync } from 'child_process';
  15. import * as fs from 'fs';
  16. import * as path from 'path';
  17. import * as os from 'os';
  18. import { CodeGraph } from '../src';
  19. const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
  20. // eslint-disable-next-line no-control-regex
  21. const ANSI = /\x1b\[/;
  22. /**
  23. * Env for spawns: neutralize ambient color signals so each case is explicit.
  24. * The vars must be DELETED, not set to '' — Node itself (and some deps) treat
  25. * a present-but-empty FORCE_COLOR as "colors on".
  26. */
  27. function colorEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
  28. const env: NodeJS.ProcessEnv = {
  29. ...process.env,
  30. CODEGRAPH_NO_DAEMON: '1',
  31. CODEGRAPH_TELEMETRY: '0',
  32. };
  33. delete env.NO_COLOR;
  34. delete env.FORCE_COLOR;
  35. delete env.CI;
  36. return { ...env, ...extra };
  37. }
  38. function run(args: string[], env: NodeJS.ProcessEnv, cwd: string): string {
  39. return execFileSync(process.execPath, [BIN, ...args], {
  40. cwd,
  41. encoding: 'utf-8',
  42. env,
  43. stdio: ['ignore', 'pipe', 'pipe'],
  44. });
  45. }
  46. describe('CLI color handling (#1281)', () => {
  47. let tempDir: string;
  48. beforeAll(async () => {
  49. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-no-color-'));
  50. fs.writeFileSync(
  51. path.join(tempDir, 'a.ts'),
  52. 'export function alpha(): number { return beta(); }\nexport function beta(): number { return 1; }\n'
  53. );
  54. const cg = CodeGraph.initSync(tempDir);
  55. await cg.indexAll();
  56. cg.close();
  57. }, 60000);
  58. afterAll(() => {
  59. fs.rmSync(tempDir, { recursive: true, force: true });
  60. });
  61. it('piped stdout gets no ANSI codes by default (status, query, callers, files)', () => {
  62. for (const args of [['status'], ['query', 'alpha'], ['callers', 'beta'], ['files']]) {
  63. const out = run(args, colorEnv(), tempDir);
  64. expect(out, `command: ${args.join(' ')}`).not.toMatch(ANSI);
  65. }
  66. });
  67. it('NO_COLOR=1 suppresses ANSI codes even when colors are forced on by env', () => {
  68. const out = run(['status'], colorEnv({ NO_COLOR: '1', FORCE_COLOR: '1' }), tempDir);
  69. expect(out).not.toMatch(ANSI);
  70. });
  71. it('FORCE_COLOR=1 emits ANSI codes even though stdout is piped', () => {
  72. const out = run(['status'], colorEnv({ FORCE_COLOR: '1' }), tempDir);
  73. expect(out).toMatch(ANSI);
  74. });
  75. it('--color forces ANSI codes on piped stdout; --no-color wins over FORCE_COLOR', () => {
  76. const forced = run(['status', '--color'], colorEnv(), tempDir);
  77. expect(forced).toMatch(ANSI);
  78. const suppressed = run(['status', '--no-color'], colorEnv({ FORCE_COLOR: '1' }), tempDir);
  79. expect(suppressed).not.toMatch(ANSI);
  80. });
  81. it('--color / --no-color are accepted in any argv position (not rejected by subcommands)', () => {
  82. // Would exit non-zero with "unknown option" if the flag reached commander.
  83. const out = run(['query', 'alpha', '--no-color'], colorEnv(), tempDir);
  84. expect(out).toContain('alpha');
  85. });
  86. it('piped `codegraph index` emits plain per-phase lines: no ANSI, no \\r rewrites', () => {
  87. const out = run(['index'], colorEnv(), tempDir);
  88. expect(out).not.toMatch(ANSI);
  89. expect(out).not.toContain('\r');
  90. }, 60000);
  91. });