Add unit tests for checklist-runner module#250
Add unit tests for checklist-runner module#250nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
Conversation
|
@nikolasdehor is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a comprehensive Jest unit test suite for the .aios-core/core/orchestration/checklist-runner module to validate checklist loading/parsing and deterministic validation execution behavior.
Changes:
- Introduces 37 unit/integration-style tests covering
ChecklistRunnerconstructor, checklist loading, markdown/YAML parsing, item normalization, evaluation, validation rules,run(), andgetSummary(). - Mocks
fs-extraandjs-yamlto keep tests deterministic and filesystem-independent.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| test('items with validation execute it', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| const item = { | ||
| description: 'File exists', | ||
| tipo: 'pre-conditions', | ||
| blocker: true, | ||
| validation: 'file exists', | ||
| }; | ||
|
|
||
| const result = await runner.evaluateItem(item, '/src/index.js'); | ||
| expect(result.passed).toBe(true); | ||
| }); |
There was a problem hiding this comment.
Most tests pass absolute target paths (e.g., "/src/app.js"). In ChecklistRunner.executeValidation the code does path.join(projectRoot, targetPath); when targetPath is absolute, projectRoot is ignored, so these tests don’t exercise the intended “projectRoot + relative path” behavior. Use relative paths (e.g., "src/app.js") and/or assert fs.pathExists/fs.stat were called with the joined full path to better match real workflow usage (phase.creates values are relative).
| expect(await runner.executeValidation('file exists', '/src/app.js')).toBe(true); | ||
| }); | ||
|
|
||
| test('file exists - fails', async () => { | ||
| fs.pathExists.mockResolvedValue(false); | ||
| expect(await runner.executeValidation('file exists', '/missing.js')).toBe(false); | ||
| }); | ||
|
|
||
| test('file exists with array paths', async () => { | ||
| fs.pathExists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); | ||
| expect(await runner.executeValidation('file exists', ['/a.js', '/b.js'])).toBe(true); | ||
| }); | ||
|
|
||
| test('directory exists - passes', async () => { | ||
| fs.stat.mockResolvedValue({ isDirectory: () => true }); | ||
| expect(await runner.executeValidation('directory exists', '/src')).toBe(true); | ||
| }); | ||
|
|
||
| test('directory exists - fails for file', async () => { | ||
| fs.stat.mockResolvedValue({ isDirectory: () => false }); | ||
| expect(await runner.executeValidation('directory exists', '/file.txt')).toBe(false); | ||
| }); | ||
|
|
||
| test('not empty - passes for non-empty file', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.readFile.mockResolvedValue('content here'); | ||
| expect(await runner.executeValidation('not empty', '/file.md')).toBe(true); | ||
| }); | ||
|
|
||
| test('not empty - fails for empty file', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.readFile.mockResolvedValue(' '); | ||
| expect(await runner.executeValidation('not empty', '/file.md')).toBe(false); | ||
| }); | ||
|
|
||
| test('contains check - passes', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.readFile.mockResolvedValue('# architecture\n## components'); | ||
| expect(await runner.executeValidation("contains 'architecture'", '/doc.md')).toBe(true); | ||
| }); | ||
|
|
||
| test('contains check - fails', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.readFile.mockResolvedValue('# empty doc'); | ||
| expect(await runner.executeValidation("contains 'architecture'", '/doc.md')).toBe(false); | ||
| }); | ||
|
|
||
| test('contains check - fails for missing file', async () => { | ||
| fs.pathExists.mockResolvedValue(false); | ||
| expect(await runner.executeValidation("contains 'something'", '/missing.md')).toBe(false); | ||
| }); | ||
|
|
||
| test('minimum size check - passes', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.stat.mockResolvedValue({ size: 500 }); | ||
| expect(await runner.executeValidation('minimum size: 100', '/file.md')).toBe(true); | ||
| }); | ||
|
|
||
| test('minimum size check - fails', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.stat.mockResolvedValue({ size: 50 }); | ||
| expect(await runner.executeValidation('min size: 100', '/file.md')).toBe(false); | ||
| }); | ||
|
|
||
| test('minimum size - fails for missing file', async () => { | ||
| fs.pathExists.mockResolvedValue(false); | ||
| expect(await runner.executeValidation('min size: 100', '/missing.md')).toBe(false); | ||
| }); | ||
|
|
||
| test('unknown validation defaults to true', async () => { | ||
| expect(await runner.executeValidation('some human description', '/file')).toBe(true); |
There was a problem hiding this comment.
executeValidation tests use absolute paths ("/src", "/missing.js", etc.), which bypass projectRoot due to path.join semantics and can hide regressions in path handling. Switch these to relative paths and add expectations on fs.pathExists/fs.stat arguments so the tests validate the constructed full paths.
| expect(await runner.executeValidation('file exists', '/src/app.js')).toBe(true); | |
| }); | |
| test('file exists - fails', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation('file exists', '/missing.js')).toBe(false); | |
| }); | |
| test('file exists with array paths', async () => { | |
| fs.pathExists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); | |
| expect(await runner.executeValidation('file exists', ['/a.js', '/b.js'])).toBe(true); | |
| }); | |
| test('directory exists - passes', async () => { | |
| fs.stat.mockResolvedValue({ isDirectory: () => true }); | |
| expect(await runner.executeValidation('directory exists', '/src')).toBe(true); | |
| }); | |
| test('directory exists - fails for file', async () => { | |
| fs.stat.mockResolvedValue({ isDirectory: () => false }); | |
| expect(await runner.executeValidation('directory exists', '/file.txt')).toBe(false); | |
| }); | |
| test('not empty - passes for non-empty file', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('content here'); | |
| expect(await runner.executeValidation('not empty', '/file.md')).toBe(true); | |
| }); | |
| test('not empty - fails for empty file', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue(' '); | |
| expect(await runner.executeValidation('not empty', '/file.md')).toBe(false); | |
| }); | |
| test('contains check - passes', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('# architecture\n## components'); | |
| expect(await runner.executeValidation("contains 'architecture'", '/doc.md')).toBe(true); | |
| }); | |
| test('contains check - fails', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('# empty doc'); | |
| expect(await runner.executeValidation("contains 'architecture'", '/doc.md')).toBe(false); | |
| }); | |
| test('contains check - fails for missing file', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation("contains 'something'", '/missing.md')).toBe(false); | |
| }); | |
| test('minimum size check - passes', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.stat.mockResolvedValue({ size: 500 }); | |
| expect(await runner.executeValidation('minimum size: 100', '/file.md')).toBe(true); | |
| }); | |
| test('minimum size check - fails', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.stat.mockResolvedValue({ size: 50 }); | |
| expect(await runner.executeValidation('min size: 100', '/file.md')).toBe(false); | |
| }); | |
| test('minimum size - fails for missing file', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation('min size: 100', '/missing.md')).toBe(false); | |
| }); | |
| test('unknown validation defaults to true', async () => { | |
| expect(await runner.executeValidation('some human description', '/file')).toBe(true); | |
| expect(await runner.executeValidation('file exists', 'src/app.js')).toBe(true); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/src/app.js'); | |
| }); | |
| test('file exists - fails', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation('file exists', 'missing.js')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/missing.js'); | |
| }); | |
| test('file exists with array paths', async () => { | |
| fs.pathExists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); | |
| expect(await runner.executeValidation('file exists', ['a.js', 'b.js'])).toBe(true); | |
| expect(fs.pathExists).toHaveBeenNthCalledWith(1, '/project/a.js'); | |
| expect(fs.pathExists).toHaveBeenNthCalledWith(2, '/project/b.js'); | |
| }); | |
| test('directory exists - passes', async () => { | |
| fs.stat.mockResolvedValue({ isDirectory: () => true }); | |
| expect(await runner.executeValidation('directory exists', 'src')).toBe(true); | |
| expect(fs.stat).toHaveBeenCalledWith('/project/src'); | |
| }); | |
| test('directory exists - fails for file', async () => { | |
| fs.stat.mockResolvedValue({ isDirectory: () => false }); | |
| expect(await runner.executeValidation('directory exists', 'file.txt')).toBe(false); | |
| expect(fs.stat).toHaveBeenCalledWith('/project/file.txt'); | |
| }); | |
| test('not empty - passes for non-empty file', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('content here'); | |
| expect(await runner.executeValidation('not empty', 'file.md')).toBe(true); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/file.md'); | |
| }); | |
| test('not empty - fails for empty file', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue(' '); | |
| expect(await runner.executeValidation('not empty', 'file.md')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/file.md'); | |
| }); | |
| test('contains check - passes', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('# architecture\n## components'); | |
| expect(await runner.executeValidation("contains 'architecture'", 'doc.md')).toBe(true); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/doc.md'); | |
| }); | |
| test('contains check - fails', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.readFile.mockResolvedValue('# empty doc'); | |
| expect(await runner.executeValidation("contains 'architecture'", 'doc.md')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/doc.md'); | |
| }); | |
| test('contains check - fails for missing file', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation("contains 'something'", 'missing.md')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/missing.md'); | |
| }); | |
| test('minimum size check - passes', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.stat.mockResolvedValue({ size: 500 }); | |
| expect(await runner.executeValidation('minimum size: 100', 'file.md')).toBe(true); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/file.md'); | |
| expect(fs.stat).toHaveBeenCalledWith('/project/file.md'); | |
| }); | |
| test('minimum size check - fails', async () => { | |
| fs.pathExists.mockResolvedValue(true); | |
| fs.stat.mockResolvedValue({ size: 50 }); | |
| expect(await runner.executeValidation('min size: 100', 'file.md')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/file.md'); | |
| expect(fs.stat).toHaveBeenCalledWith('/project/file.md'); | |
| }); | |
| test('minimum size - fails for missing file', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation('min size: 100', 'missing.md')).toBe(false); | |
| expect(fs.pathExists).toHaveBeenCalledWith('/project/missing.md'); | |
| }); | |
| test('unknown validation defaults to true', async () => { | |
| expect(await runner.executeValidation('some human description', 'file')).toBe(true); |
| fs.readFile.mockResolvedValue(' '); | ||
| expect(await runner.executeValidation('not empty', '/file.md')).toBe(false); | ||
| }); | ||
|
|
There was a problem hiding this comment.
There’s no test covering executeValidation('not empty', ). The current implementation returns true when the file does not exist (it only fails if the file exists and is empty), which is inconsistent with the 'contains' and 'min size' rules that fail on missing files. Add a test for the missing-file case to lock in the intended behavior (and adjust implementation if missing should be treated as failure).
| test('not empty - fails for missing file', async () => { | |
| fs.pathExists.mockResolvedValue(false); | |
| expect(await runner.executeValidation('not empty', '/missing.md')).toBe(false); | |
| }); |
| test('runs checklist and passes', async () => { | ||
| fs.pathExists.mockResolvedValue(true); | ||
| fs.readFile.mockResolvedValue('- [ ] Review code\n- [ ] Check formatting'); | ||
|
|
||
| const result = await runner.run('quality', '/src/app.js'); | ||
|
|
||
| expect(result.passed).toBe(true); | ||
| expect(result.items).toHaveLength(2); | ||
| expect(result.timestamp).toBeDefined(); | ||
| }); | ||
|
|
||
| test('fails when blocker item fails', async () => { | ||
| fs.pathExists.mockImplementation((p) => { | ||
| if (p.includes('checklists')) return true; | ||
| return false; | ||
| }); | ||
| fs.readFile.mockResolvedValue('```yaml\npre-conditions:\n - Output exists\n```'); | ||
| yaml.load.mockReturnValue({ | ||
| 'pre-conditions': [{ | ||
| 'Output exists': null, | ||
| validation: 'file exists', | ||
| blocker: true, | ||
| }], | ||
| }); | ||
|
|
||
| const result = await runner.run('pre-check', '/output.md'); | ||
|
|
||
| expect(result.passed).toBe(false); | ||
| expect(result.errors.length).toBeGreaterThan(0); | ||
| }); |
There was a problem hiding this comment.
The run() integration tests also pass absolute target paths (e.g., "/src/app.js", "/output.md"), so they don’t validate the common usage where target paths are relative to projectRoot (as provided by workflow phase.creates). Prefer relative paths here too, and consider asserting that the blocker failure is specifically the expected "Blocker failed: …" error to make the test more precise.
|
Consolidated into #426 |
Summary
checklist-runnerorchestration moduleTest Coverage
Test plan
Closes #310