validate-suites-cli.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. /**
  2. * Tests for the validate-suites CLI helpers.
  3. *
  4. * Exercises the pure, exported discovery and exit-code functions using temp
  5. * directory fixtures (mkdtempSync). No CLI/process.exit is triggered because
  6. * the module uses an ESM entrypoint guard, so importing it is side-effect free.
  7. *
  8. * NOTE: Filesystem-only. No network, model, paid API, or agent execution.
  9. */
  10. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  11. import { mkdtempSync, mkdirSync, rmSync } from 'fs';
  12. import { join, dirname } from 'path';
  13. import { fileURLToPath } from 'url';
  14. import { tmpdir } from 'os';
  15. import {
  16. discoverAgents,
  17. computeExitCode
  18. } from '../validate-suites-cli.js';
  19. const __filename = fileURLToPath(import.meta.url);
  20. const __dirname = dirname(__filename);
  21. // Real agents directory (matches the layout used by the validate:suites CLI).
  22. const realAgentsDir = join(__dirname, '../../../../agents');
  23. describe('validate-suites-cli - discoverAgents', () => {
  24. let tmpRoot: string;
  25. beforeEach(() => {
  26. tmpRoot = mkdtempSync(join(tmpdir(), 'discover-agents-'));
  27. });
  28. afterEach(() => {
  29. rmSync(tmpRoot, { recursive: true, force: true });
  30. });
  31. it('recursively finds category-based agent config dirs and ignores non-agent siblings', () => {
  32. // Arrange: nested <category>/<agent>/config layout.
  33. mkdirSync(join(tmpRoot, 'core', 'openagent', 'config'), { recursive: true });
  34. mkdirSync(join(tmpRoot, 'core', 'opencoder', 'config'), { recursive: true });
  35. mkdirSync(join(tmpRoot, 'development', 'frontend-specialist', 'config'), { recursive: true });
  36. // Sibling directory WITHOUT a config subdir -> must be ignored.
  37. mkdirSync(join(tmpRoot, 'content', 'copywriter', 'docs'), { recursive: true });
  38. // Noise directories that must be skipped.
  39. mkdirSync(join(tmpRoot, '.hidden', 'config'), { recursive: true });
  40. mkdirSync(join(tmpRoot, 'node_modules', 'pkg', 'config'), { recursive: true });
  41. // Act
  42. const agents = discoverAgents(tmpRoot);
  43. // Assert: category-based ids, sorted, deterministic; no non-agent dirs.
  44. expect(agents).toEqual([
  45. 'core/opencoder',
  46. 'core/openagent',
  47. 'development/frontend-specialist'
  48. ].sort());
  49. expect(agents).not.toContain('content/copywriter');
  50. expect(agents).not.toContain('.hidden');
  51. expect(agents.some(a => a.includes('node_modules'))).toBe(false);
  52. });
  53. it('returns a sorted, deterministic order', () => {
  54. // Arrange
  55. mkdirSync(join(tmpRoot, 'zeta', 'zzz', 'config'), { recursive: true });
  56. mkdirSync(join(tmpRoot, 'alpha', 'aaa', 'config'), { recursive: true });
  57. mkdirSync(join(tmpRoot, 'core', 'mmm', 'config'), { recursive: true });
  58. // Act
  59. const agents = discoverAgents(tmpRoot);
  60. // Assert
  61. expect(agents).toEqual([...agents].sort());
  62. expect(agents).toEqual(['alpha/aaa', 'core/mmm', 'zeta/zzz']);
  63. });
  64. it('returns an empty array for an empty tree', () => {
  65. // Arrange: tmpRoot exists but has no agent config dirs.
  66. // Act
  67. const agents = discoverAgents(tmpRoot);
  68. // Assert
  69. expect(agents).toEqual([]);
  70. });
  71. it('returns an empty array for a nonexistent directory', () => {
  72. // Act
  73. const agents = discoverAgents(join(tmpRoot, 'does-not-exist'));
  74. // Assert
  75. expect(agents).toEqual([]);
  76. });
  77. it('discovers core/openagent in the real agents directory', () => {
  78. // Act
  79. const agents = discoverAgents(realAgentsDir);
  80. // Assert
  81. expect(agents).toContain('core/openagent');
  82. });
  83. });
  84. describe('validate-suites-cli - computeExitCode (fail-closed)', () => {
  85. it('returns nonzero when zero suites were discovered', () => {
  86. // Assert
  87. expect(computeExitCode({ totalSuites: 0, invalidSuites: 0 })).not.toBe(0);
  88. });
  89. it('returns nonzero when any suite is invalid', () => {
  90. // Assert
  91. expect(computeExitCode({ totalSuites: 5, invalidSuites: 1 })).not.toBe(0);
  92. });
  93. it('returns zero only when total > 0 and every suite is valid', () => {
  94. // Assert
  95. expect(computeExitCode({ totalSuites: 3, invalidSuites: 0 })).toBe(0);
  96. });
  97. it('treats zero suites as a failure even with no invalid suites', () => {
  98. // Assert: fail-closed — empty discovery must not be reported as success.
  99. expect(computeExitCode({ totalSuites: 0, invalidSuites: 0 })).toBe(1);
  100. });
  101. });