test: add comprehensive unit tests for tech-stack-detector#226
test: add comprehensive unit tests for tech-stack-detector#226nikolasdehor 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. |
|
No actionable comments were generated in the recent review. 🎉 WalkthroughAdds a comprehensive unit test suite for TechStackDetector exercising constructor behavior, detection helpers (databases, frontend, backend, TypeScript, tests), utility methods, caching, and error handling via mocked filesystem interactions. No production code or public API changes. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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.
🧹 Nitpick comments (3)
tests/core/orchestration/tech-stack-detector.test.js (3)
22-27: Usejest.resetAllMocks()(or restore all four mocks inbeforeEach) to prevent stale implementation bleed.
jest.clearAllMocks()only wipes call history — it does not resetmockImplementationormockResolvedValueoverrides. Right now,fs.readJson,fs.readdir, andfs.readFilesilently retain whatever implementation the previous test left behind. OnlypathExistsis explicitly patched back.Current bleed path: the RLS test (line 192) writes
fs.readdir.mockResolvedValue(['001_init.sql'])and the.tsxtest (line 341) writes['App.tsx', 'index.ts']; both values persist untouched into every subsequent describe block that never touchesreaddir.♻️ Proposed fix
beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); detector = new TechStackDetector(ROOT); - // Default: no package.json - fs.pathExists.mockResolvedValue(false); + // Restore factory defaults after resetAllMocks() + fs.pathExists.mockResolvedValue(false); + fs.readJson.mockResolvedValue(null); + fs.readdir.mockResolvedValue([]); + fs.readFile.mockResolvedValue(''); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/core/orchestration/tech-stack-detector.test.js` around lines 22 - 27, Replace jest.clearAllMocks() with jest.resetAllMocks() (or explicitly restore mocks) in the beforeEach that constructs TechStackDetector so mock implementations from previous tests (e.g., fs.readJson, fs.readdir, fs.readFile) are reset; ensure fs.pathExists remains mocked to false as currently done. This targets the beforeEach block that sets up detector = new TechStackDetector(ROOT) and currently calls jest.clearAllMocks(), and fixes bleed from tests setting fs.readdir.mockResolvedValue(...) and similar.
42-49:p.includes(key)substring matching is ambiguous — prefer exact path comparison.For example,
mockPaths({ 'api': true })will also returntruefor/test/project/pages/api,/test/project/someapi, etc. Whether those false positives matter depends entirely on which paths the implementation checks, making the test's fidelity fragile. Also, if two keys match the same resolved path (e.g.,{ 'package.json': false, 'supabase': true }against/supabase/package.json), the result is decided by key insertion order rather than intent.♻️ Proposed fix — exact path matching
function mockPaths(pathMap) { fs.pathExists.mockImplementation(async (p) => { - for (const [key, val] of Object.entries(pathMap)) { - if (p.includes(key)) return val; - } - return false; + const resolved = Object.fromEntries( + Object.entries(pathMap).map(([key, val]) => [path.join(ROOT, key), val]) + ); + return resolved[p] ?? false; }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/core/orchestration/tech-stack-detector.test.js` around lines 42 - 49, The mockPaths helper currently uses substring matching (p.includes(key)) which causes false positives; update mockPaths to perform exact path comparisons by resolving/normalizing both the incoming path and each key (use path.resolve or path.normalize) and then compare for strict equality when implementing fs.pathExists.mockImplementation in mockPaths; ensure keys in pathMap are resolved once (inside mockPaths) so lookups use resolvedKey === resolvedP to decide true/false rather than substring matching.
274-288: Build-tool tests omithasFrontendassertion and place tools in the wrong dependency bucket.Two related issues:
Neither the
vitenorwebpacktest assertsprofile.hasFrontend. If the implementation does sethasFrontendon build-tool-only detection, the behavior is unverified. If it intentionally does not, adding atoBe(false)assertion makes that contract explicit and prevents silent regressions.
viteandwebpackare passed as runtimedependencies(first argument tomockPackageJson), notdevDependencies. These are always dev tools. If the production code ever narrows its lookup todevDependenciesonly, these tests would give a false green. Move them to the second argument.♻️ Proposed fix
- it('should detect vite build tool', async () => { - mockPackageJson({ vite: '^5.0.0' }); + it('should detect vite build tool', async () => { + mockPackageJson({}, { vite: '^5.0.0' }); const profile = detector._createEmptyProfile(); await detector._detectFrontend(profile); expect(profile.frontend.buildTool).toBe('vite'); + expect(profile.hasFrontend).toBe(false); // or true — assert the actual contract }); - it('should detect webpack build tool', async () => { - mockPackageJson({ webpack: '^5.0.0' }); + it('should detect webpack build tool', async () => { + mockPackageJson({}, { webpack: '^5.0.0' }); const profile = detector._createEmptyProfile(); await detector._detectFrontend(profile); expect(profile.frontend.buildTool).toBe('webpack'); + expect(profile.hasFrontend).toBe(false); // or true — assert the actual contract });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/core/orchestration/tech-stack-detector.test.js` around lines 274 - 288, Update the two build-tool tests to assert the frontend presence and to mock the tools as devDependencies: call mockPackageJson with vite/webpack in the second argument (devDependencies) instead of the first, then after creating profile via detector._createEmptyProfile() and running detector._detectFrontend(profile) add an assertion on profile.hasFrontend (explicitly expect(false) if the intended contract is that build-tool-only detection does not mark hasFrontend) and keep the existing expect(profile.frontend.buildTool).toBe('vite'/'webpack') checks; this ensures the tests reflect dev-only tooling and verify hasFrontend behavior for detector._detectFrontend.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/core/orchestration/tech-stack-detector.test.js`:
- Around line 22-27: Replace jest.clearAllMocks() with jest.resetAllMocks() (or
explicitly restore mocks) in the beforeEach that constructs TechStackDetector so
mock implementations from previous tests (e.g., fs.readJson, fs.readdir,
fs.readFile) are reset; ensure fs.pathExists remains mocked to false as
currently done. This targets the beforeEach block that sets up detector = new
TechStackDetector(ROOT) and currently calls jest.clearAllMocks(), and fixes
bleed from tests setting fs.readdir.mockResolvedValue(...) and similar.
- Around line 42-49: The mockPaths helper currently uses substring matching
(p.includes(key)) which causes false positives; update mockPaths to perform
exact path comparisons by resolving/normalizing both the incoming path and each
key (use path.resolve or path.normalize) and then compare for strict equality
when implementing fs.pathExists.mockImplementation in mockPaths; ensure keys in
pathMap are resolved once (inside mockPaths) so lookups use resolvedKey ===
resolvedP to decide true/false rather than substring matching.
- Around line 274-288: Update the two build-tool tests to assert the frontend
presence and to mock the tools as devDependencies: call mockPackageJson with
vite/webpack in the second argument (devDependencies) instead of the first, then
after creating profile via detector._createEmptyProfile() and running
detector._detectFrontend(profile) add an assertion on profile.hasFrontend
(explicitly expect(false) if the intended contract is that build-tool-only
detection does not mark hasFrontend) and keep the existing
expect(profile.frontend.buildTool).toBe('vite'/'webpack') checks; this ensures
the tests reflect dev-only tooling and verify hasFrontend behavior for
detector._detectFrontend.
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive unit test coverage for the TechStackDetector class, a critical deterministic module responsible for detecting project technology stacks (database, frontend, backend, TypeScript, and test frameworks) before workflow execution. The tests properly mock the fs-extra module to ensure deterministic behavior and fast execution.
Changes:
- Add 73 unit tests covering all major detection methods and edge cases
- Implement mock helpers for fs-extra operations (pathExists, readJson, readdir, readFile)
- Test database detection for 6 types (supabase, prisma, postgresql, mongodb, mysql, sqlite)
- Test frontend detection for 6 frameworks and their associated build tools/styling
- Test backend, TypeScript, and test framework detection
- Test phase computation, confidence scoring, and summary formatting
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('postgresql'); |
There was a problem hiding this comment.
The test doesn't verify that profile.database.hasSchema is set to true when a prisma/schema.prisma file exists. According to the implementation (line 229), hasSchema should be true when schema.prisma exists. Add an assertion: expect(profile.database.hasSchema).toBe(true);
| expect(profile.database.type).toBe('postgresql'); | |
| expect(profile.database.type).toBe('postgresql'); | |
| expect(profile.database.hasSchema).toBe(true); |
| fs.readFile.mockResolvedValue('CREATE TABLE t(); ENABLE ROW LEVEL SECURITY;'); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); |
There was a problem hiding this comment.
The test doesn't verify that hasMigrations is set to true. According to the implementation (lines 196-198), when migrations directory exists, both hasMigrations and hasSchema should be set to true. Add assertions for these fields to ensure complete test coverage.
| await detector._detectDatabase(profile); | |
| await detector._detectDatabase(profile); | |
| expect(profile.database.hasMigrations).toBe(true); | |
| expect(profile.database.hasSchema).toBe(true); |
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.buildTool).toBe('webpack'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The implementation (lines 338-341) also checks for 'esbuild' and 'parcel' build tools, but tests only cover 'vite' and 'webpack'. Add test cases for esbuild and parcel to achieve complete coverage of the build tool detection logic.
| it('should detect esbuild build tool', async () => { | |
| mockPackageJson({ esbuild: '^0.20.0' }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectFrontend(profile); | |
| expect(profile.frontend.buildTool).toBe('esbuild'); | |
| }); | |
| it('should detect parcel build tool', async () => { | |
| mockPackageJson({ parcel: '^2.0.0' }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectFrontend(profile); | |
| expect(profile.frontend.buildTool).toBe('parcel'); | |
| }); |
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.backend.hasAPI).toBe(true); | ||
| }); |
There was a problem hiding this comment.
The implementation checks for multiple backend API paths including 'src/api' and 'app/api' (Next.js App Router) as shown in lines 427-432, but tests only cover the 'api' and 'pages/api' directories. Add tests for 'src/api' and 'app/api' to ensure all API path detection logic is covered.
| }); | |
| }); | |
| it('should detect API routes from src/api (Next.js src structure)', async () => { | |
| mockPaths({ 'package.json': false, 'src/api': true }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectBackend(profile); | |
| expect(profile.backend.hasAPI).toBe(true); | |
| }); | |
| it('should detect API routes from app/api (Next.js App Router)', async () => { | |
| mockPaths({ 'package.json': false, 'app/api': true }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectBackend(profile); | |
| expect(profile.backend.hasAPI).toBe(true); | |
| }); |
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTests(profile); | ||
| expect(profile.hasTests).toBe(true); | ||
| }); |
There was a problem hiding this comment.
The implementation checks for additional test directories: 'test' (singular) and 'src/tests' (lines 489-494), but tests only cover 'tests' and 'tests'. Add test cases for these other directory names to ensure complete coverage.
| }); | |
| }); | |
| it('should detect from test directory', async () => { | |
| mockPaths({ 'package.json': false, 'test': true }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectTests(profile); | |
| expect(profile.hasTests).toBe(true); | |
| }); | |
| it('should detect from src/__tests__ directory', async () => { | |
| mockPaths({ 'package.json': false, 'src/__tests__': true }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectTests(profile); | |
| expect(profile.hasTests).toBe(true); | |
| }); |
| // ─── _detectDatabase ────────────────────────────────────────── | ||
|
|
||
| describe('_detectDatabase', () => { | ||
| it('should detect supabase from directory', async () => { | ||
| mockPaths({ 'package.json': true, 'supabase': true }); | ||
| fs.readJson.mockResolvedValue({ dependencies: {}, devDependencies: {} }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('supabase'); | ||
| }); | ||
|
|
||
| it('should detect supabase from dependency', async () => { | ||
| mockPackageJson({ '@supabase/supabase-js': '^2.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('supabase'); | ||
| }); | ||
|
|
||
| it('should detect prisma/postgresql from directory', async () => { | ||
| mockPaths({ 'package.json': true, 'prisma': true, 'schema.prisma': true }); | ||
| fs.readJson.mockResolvedValue({ dependencies: {}, devDependencies: {} }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('postgresql'); | ||
| }); | ||
|
|
||
| it('should detect postgresql from pg dependency', async () => { | ||
| mockPackageJson({ pg: '^8.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('postgresql'); | ||
| }); | ||
|
|
||
| it('should detect mongodb from mongoose dependency', async () => { | ||
| mockPackageJson({ mongoose: '^7.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('mongodb'); | ||
| }); | ||
|
|
||
| it('should detect mysql from mysql2 dependency', async () => { | ||
| mockPackageJson({ mysql2: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('mysql'); | ||
| }); | ||
|
|
||
| it('should detect sqlite from better-sqlite3', async () => { | ||
| mockPackageJson({ 'better-sqlite3': '^9.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('sqlite'); | ||
| }); | ||
|
|
||
| it('should detect RLS from migration SQL', async () => { | ||
| mockPaths({ 'package.json': false, 'supabase': true, 'migrations': true }); | ||
| fs.readdir.mockResolvedValue(['001_init.sql']); | ||
| fs.readFile.mockResolvedValue('CREATE TABLE t(); ENABLE ROW LEVEL SECURITY;'); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.database.hasRLS).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect env vars from .env files', async () => { | ||
| mockPaths({ 'package.json': false, '.env': true }); | ||
| fs.readFile.mockResolvedValue('DATABASE_URL=postgres://localhost/db'); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.database.envVarsConfigured).toBe(true); | ||
| }); | ||
|
|
||
| it('should not set database when no indicators found', async () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(false); | ||
| expect(profile.database.type).toBeNull(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The implementation handles file system errors gracefully with try-catch blocks (e.g., when reading migration files or env files), but there are no tests that verify error handling behavior. Consider adding test cases where fs.readFile or fs.readdir throw errors to ensure the detector continues functioning and doesn't crash when encountering file system errors.
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.componentLibrary).toBe('mui'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The implementation (lines 359-363) also checks for '@material-ui/core', 'chakra-ui/react', and 'antd' component libraries, but tests only cover 'shadcn' and 'mui'. Add test cases for Chakra UI and Ant Design to ensure complete coverage.
| it('should detect Chakra UI component library', async () => { | |
| mockPackageJson({ 'chakra-ui/react': '^2.0.0' }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectFrontend(profile); | |
| expect(profile.frontend.componentLibrary).toBe('chakra'); | |
| }); | |
| it('should detect Ant Design component library', async () => { | |
| mockPackageJson({ antd: '^5.0.0' }); | |
| const profile = detector._createEmptyProfile(); | |
| await detector._detectFrontend(profile); | |
| expect(profile.frontend.componentLibrary).toBe('antd'); | |
| }); |
| /** | ||
| * Tests for TechStackDetector | ||
| * @see .aios-core/core/orchestration/tech-stack-detector.js | ||
| */ | ||
|
|
||
| const path = require('path'); | ||
|
|
||
| jest.mock('fs-extra', () => ({ | ||
| pathExists: jest.fn().mockResolvedValue(false), | ||
| readJson: jest.fn().mockResolvedValue(null), | ||
| readdir: jest.fn().mockResolvedValue([]), | ||
| readFile: jest.fn().mockResolvedValue(''), | ||
| })); | ||
|
|
||
| const fs = require('fs-extra'); | ||
| const TechStackDetector = require('../../../.aios-core/core/orchestration/tech-stack-detector'); | ||
|
|
||
| describe('TechStackDetector', () => { | ||
| let detector; | ||
| const ROOT = '/test/project'; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| detector = new TechStackDetector(ROOT); | ||
| // Default: no package.json | ||
| fs.pathExists.mockResolvedValue(false); | ||
| }); | ||
|
|
||
| // Helper to mock package.json with specific deps | ||
| function mockPackageJson(deps = {}, devDeps = {}) { | ||
| fs.pathExists.mockImplementation(async (p) => { | ||
| if (p === path.join(ROOT, 'package.json')) return true; | ||
| return false; | ||
| }); | ||
| fs.readJson.mockResolvedValue({ | ||
| dependencies: deps, | ||
| devDependencies: devDeps, | ||
| }); | ||
| } | ||
|
|
||
| // Helper to mock pathExists for specific paths | ||
| function mockPaths(pathMap) { | ||
| fs.pathExists.mockImplementation(async (p) => { | ||
| for (const [key, val] of Object.entries(pathMap)) { | ||
| if (p.includes(key)) return val; | ||
| } | ||
| return false; | ||
| }); | ||
| } | ||
|
|
||
| // ─── Constructor ─────────────────────────────────────────────── | ||
|
|
||
| describe('constructor', () => { | ||
| it('should store project root', () => { | ||
| expect(detector.projectRoot).toBe(ROOT); | ||
| }); | ||
|
|
||
| it('should initialize package.json cache as null', () => { | ||
| expect(detector._packageJsonCache).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _createEmptyProfile ────────────────────────────────────── | ||
|
|
||
| describe('_createEmptyProfile', () => { | ||
| it('should return profile with all flags false', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| expect(profile.hasDatabase).toBe(false); | ||
| expect(profile.hasFrontend).toBe(false); | ||
| expect(profile.hasBackend).toBe(false); | ||
| expect(profile.hasTypeScript).toBe(false); | ||
| expect(profile.hasTests).toBe(false); | ||
| }); | ||
|
|
||
| it('should return profile with null database type', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| expect(profile.database.type).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return profile with empty applicablePhases', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| expect(profile.applicablePhases).toEqual([]); | ||
| }); | ||
|
|
||
| it('should return confidence 0', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| expect(profile.confidence).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── detect (integration) ──────────────────────────────────── | ||
|
|
||
| describe('detect', () => { | ||
| it('should return a complete profile', async () => { | ||
| const profile = await detector.detect(); | ||
| expect(profile).toHaveProperty('hasDatabase'); | ||
| expect(profile).toHaveProperty('hasFrontend'); | ||
| expect(profile).toHaveProperty('hasBackend'); | ||
| expect(profile).toHaveProperty('hasTypeScript'); | ||
| expect(profile).toHaveProperty('hasTests'); | ||
| expect(profile).toHaveProperty('applicablePhases'); | ||
| expect(profile).toHaveProperty('confidence'); | ||
| expect(profile).toHaveProperty('detectedAt'); | ||
| }); | ||
|
|
||
| it('should set detectedAt as ISO timestamp', async () => { | ||
| const profile = await detector.detect(); | ||
| expect(profile.detectedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); | ||
| }); | ||
|
|
||
| it('should always include phase 1 in applicable phases', async () => { | ||
| const profile = await detector.detect(); | ||
| expect(profile.applicablePhases).toContain(1); | ||
| }); | ||
|
|
||
| it('should have base confidence of 50 with no detections', async () => { | ||
| const profile = await detector.detect(); | ||
| expect(profile.confidence).toBe(50); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _detectDatabase ────────────────────────────────────────── | ||
|
|
||
| describe('_detectDatabase', () => { | ||
| it('should detect supabase from directory', async () => { | ||
| mockPaths({ 'package.json': true, 'supabase': true }); | ||
| fs.readJson.mockResolvedValue({ dependencies: {}, devDependencies: {} }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('supabase'); | ||
| }); | ||
|
|
||
| it('should detect supabase from dependency', async () => { | ||
| mockPackageJson({ '@supabase/supabase-js': '^2.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('supabase'); | ||
| }); | ||
|
|
||
| it('should detect prisma/postgresql from directory', async () => { | ||
| mockPaths({ 'package.json': true, 'prisma': true, 'schema.prisma': true }); | ||
| fs.readJson.mockResolvedValue({ dependencies: {}, devDependencies: {} }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('postgresql'); | ||
| }); | ||
|
|
||
| it('should detect postgresql from pg dependency', async () => { | ||
| mockPackageJson({ pg: '^8.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('postgresql'); | ||
| }); | ||
|
|
||
| it('should detect mongodb from mongoose dependency', async () => { | ||
| mockPackageJson({ mongoose: '^7.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('mongodb'); | ||
| }); | ||
|
|
||
| it('should detect mysql from mysql2 dependency', async () => { | ||
| mockPackageJson({ mysql2: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('mysql'); | ||
| }); | ||
|
|
||
| it('should detect sqlite from better-sqlite3', async () => { | ||
| mockPackageJson({ 'better-sqlite3': '^9.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(true); | ||
| expect(profile.database.type).toBe('sqlite'); | ||
| }); | ||
|
|
||
| it('should detect RLS from migration SQL', async () => { | ||
| mockPaths({ 'package.json': false, 'supabase': true, 'migrations': true }); | ||
| fs.readdir.mockResolvedValue(['001_init.sql']); | ||
| fs.readFile.mockResolvedValue('CREATE TABLE t(); ENABLE ROW LEVEL SECURITY;'); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.database.hasRLS).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect env vars from .env files', async () => { | ||
| mockPaths({ 'package.json': false, '.env': true }); | ||
| fs.readFile.mockResolvedValue('DATABASE_URL=postgres://localhost/db'); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.database.envVarsConfigured).toBe(true); | ||
| }); | ||
|
|
||
| it('should not set database when no indicators found', async () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectDatabase(profile); | ||
| expect(profile.hasDatabase).toBe(false); | ||
| expect(profile.database.type).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _detectFrontend ────────────────────────────────────────── | ||
|
|
||
| describe('_detectFrontend', () => { | ||
| it('should detect react', async () => { | ||
| mockPackageJson({ react: '^18.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('react'); | ||
| }); | ||
|
|
||
| it('should detect vue', async () => { | ||
| mockPackageJson({ vue: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('vue'); | ||
| }); | ||
|
|
||
| it('should detect angular', async () => { | ||
| mockPackageJson({ '@angular/core': '^17.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('angular'); | ||
| }); | ||
|
|
||
| it('should detect svelte', async () => { | ||
| mockPackageJson({ svelte: '^4.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('svelte'); | ||
| }); | ||
|
|
||
| it('should detect next.js as react', async () => { | ||
| mockPackageJson({ next: '^14.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('react'); | ||
| }); | ||
|
|
||
| it('should detect nuxt as vue', async () => { | ||
| mockPackageJson({ nuxt: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| expect(profile.frontend.framework).toBe('vue'); | ||
| }); | ||
|
|
||
| it('should detect vite build tool', async () => { | ||
| mockPackageJson({ vite: '^5.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.buildTool).toBe('vite'); | ||
| }); | ||
|
|
||
| it('should detect webpack build tool', async () => { | ||
| mockPackageJson({ webpack: '^5.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.buildTool).toBe('webpack'); | ||
| }); | ||
|
|
||
| it('should detect tailwind styling', async () => { | ||
| mockPackageJson({ tailwindcss: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.styling).toBe('tailwind'); | ||
| }); | ||
|
|
||
| it('should detect styled-components', async () => { | ||
| mockPackageJson({ 'styled-components': '^6.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.styling).toBe('styled-components'); | ||
| }); | ||
|
|
||
| it('should detect emotion', async () => { | ||
| mockPackageJson({ '@emotion/react': '^11.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.styling).toBe('emotion'); | ||
| }); | ||
|
|
||
| it('should detect scss/sass', async () => { | ||
| mockPackageJson({ sass: '^1.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.styling).toBe('scss'); | ||
| }); | ||
|
|
||
| it('should detect shadcn from components/ui directory', async () => { | ||
| mockPaths({ 'package.json': true, 'src/components/ui': true }); | ||
| fs.readJson.mockResolvedValue({ dependencies: { react: '^18' }, devDependencies: {} }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.componentLibrary).toBe('shadcn'); | ||
| }); | ||
|
|
||
| it('should detect MUI component library', async () => { | ||
| mockPackageJson({ '@mui/material': '^5.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.frontend.componentLibrary).toBe('mui'); | ||
| }); | ||
|
|
||
| it('should detect frontend from .tsx files in src', async () => { | ||
| mockPaths({ 'package.json': false, 'src': true }); | ||
| fs.readdir.mockResolvedValue(['App.tsx', 'index.ts']); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectFrontend(profile); | ||
| expect(profile.hasFrontend).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _detectBackend ─────────────────────────────────────────── | ||
|
|
||
| describe('_detectBackend', () => { | ||
| it('should detect express', async () => { | ||
| mockPackageJson({ express: '^4.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('express'); | ||
| }); | ||
|
|
||
| it('should detect fastify', async () => { | ||
| mockPackageJson({ fastify: '^4.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('fastify'); | ||
| }); | ||
|
|
||
| it('should detect nestjs', async () => { | ||
| mockPackageJson({ '@nestjs/core': '^10.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('nest'); | ||
| }); | ||
|
|
||
| it('should detect hono', async () => { | ||
| mockPackageJson({ hono: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('hono'); | ||
| }); | ||
|
|
||
| it('should detect edge functions from supabase/functions directory', async () => { | ||
| mockPaths({ 'package.json': false, 'supabase/functions': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('edge-functions'); | ||
| }); | ||
|
|
||
| it('should detect API routes from api directory', async () => { | ||
| mockPaths({ 'package.json': false, 'api': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.backend.hasAPI).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect API routes from pages/api (Next.js)', async () => { | ||
| mockPaths({ 'package.json': false, 'pages/api': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.backend.hasAPI).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _detectTypeScript ──────────────────────────────────────── | ||
|
|
||
| describe('_detectTypeScript', () => { | ||
| it('should detect from typescript dependency', async () => { | ||
| mockPackageJson({}, { typescript: '^5.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTypeScript(profile); | ||
| expect(profile.hasTypeScript).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect from tsconfig.json', async () => { | ||
| mockPaths({ 'package.json': false, 'tsconfig.json': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTypeScript(profile); | ||
| expect(profile.hasTypeScript).toBe(true); | ||
| }); | ||
|
|
||
| it('should not detect when no indicators', async () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTypeScript(profile); | ||
| expect(profile.hasTypeScript).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _detectTests ───────────────────────────────────────────── | ||
|
|
||
| describe('_detectTests', () => { | ||
| it('should detect jest', async () => { | ||
| mockPackageJson({}, { jest: '^30.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTests(profile); | ||
| expect(profile.hasTests).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect vitest', async () => { | ||
| mockPackageJson({}, { vitest: '^1.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTests(profile); | ||
| expect(profile.hasTests).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect from tests directory', async () => { | ||
| mockPaths({ 'package.json': false, 'tests': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTests(profile); | ||
| expect(profile.hasTests).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect from __tests__ directory', async () => { | ||
| mockPaths({ 'package.json': false, '__tests__': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectTests(profile); | ||
| expect(profile.hasTests).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _computeApplicablePhases ───────────────────────────────── | ||
|
|
||
| describe('_computeApplicablePhases', () => { | ||
| it('should always include phase 1', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| detector._computeApplicablePhases(profile); | ||
| expect(profile.applicablePhases).toContain(1); | ||
| }); | ||
|
|
||
| it('should include phase 2 when hasDatabase', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| detector._computeApplicablePhases(profile); | ||
| expect(profile.applicablePhases).toContain(2); | ||
| }); | ||
|
|
||
| it('should not include phase 2 without database', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| detector._computeApplicablePhases(profile); | ||
| expect(profile.applicablePhases).not.toContain(2); | ||
| }); | ||
|
|
||
| it('should include phase 3 when hasFrontend', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasFrontend = true; | ||
| detector._computeApplicablePhases(profile); | ||
| expect(profile.applicablePhases).toContain(3); | ||
| }); | ||
|
|
||
| it('should not include phase 3 without frontend', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| detector._computeApplicablePhases(profile); | ||
| expect(profile.applicablePhases).not.toContain(3); | ||
| }); | ||
|
|
||
| it('should always include phases 4-10', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| detector._computeApplicablePhases(profile); | ||
| for (let i = 4; i <= 10; i++) { | ||
| expect(profile.applicablePhases).toContain(i); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _calculateConfidence ───────────────────────────────────── | ||
|
|
||
| describe('_calculateConfidence', () => { | ||
| it('should have base confidence of 50', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(50); | ||
| }); | ||
|
|
||
| it('should add 10 for database detection', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBeGreaterThanOrEqual(60); | ||
| }); | ||
|
|
||
| it('should add more for database with type and env', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| profile.database.type = 'supabase'; | ||
| profile.database.envVarsConfigured = true; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(70); // 50 + 10 + 5 + 5 | ||
| }); | ||
|
|
||
| it('should add for frontend with framework, build, and styling', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasFrontend = true; | ||
| profile.frontend.framework = 'react'; | ||
| profile.frontend.buildTool = 'vite'; | ||
| profile.frontend.styling = 'tailwind'; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(70); // 50 + 10 + 5 + 3 + 2 | ||
| }); | ||
|
|
||
| it('should add for backend', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasBackend = true; | ||
| profile.backend.type = 'express'; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(58); // 50 + 5 + 3 | ||
| }); | ||
|
|
||
| it('should add 3 for TypeScript', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasTypeScript = true; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(53); | ||
| }); | ||
|
|
||
| it('should add 2 for tests', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasTests = true; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBe(52); | ||
| }); | ||
|
|
||
| it('should cap confidence at 100', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| profile.database.type = 'supabase'; | ||
| profile.database.envVarsConfigured = true; | ||
| profile.hasFrontend = true; | ||
| profile.frontend.framework = 'react'; | ||
| profile.frontend.buildTool = 'vite'; | ||
| profile.frontend.styling = 'tailwind'; | ||
| profile.hasBackend = true; | ||
| profile.backend.type = 'express'; | ||
| profile.hasTypeScript = true; | ||
| profile.hasTests = true; | ||
| detector._calculateConfidence(profile); | ||
| expect(profile.confidence).toBeLessThanOrEqual(100); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── getSummary (static) ────────────────────────────────────── | ||
|
|
||
| describe('getSummary', () => { | ||
| it('should return "No stack detected" when nothing found', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| expect(TechStackDetector.getSummary(profile)).toBe('No stack detected'); | ||
| }); | ||
|
|
||
| it('should include frontend with framework', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasFrontend = true; | ||
| profile.frontend.framework = 'react'; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('Frontend: react'); | ||
| }); | ||
|
|
||
| it('should include frontend with styling', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasFrontend = true; | ||
| profile.frontend.framework = 'react'; | ||
| profile.frontend.styling = 'tailwind'; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('react + tailwind'); | ||
| }); | ||
|
|
||
| it('should include database with type', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| profile.database.type = 'supabase'; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('Database: supabase'); | ||
| }); | ||
|
|
||
| it('should include database with RLS', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasDatabase = true; | ||
| profile.database.type = 'supabase'; | ||
| profile.database.hasRLS = true; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('(RLS)'); | ||
| }); | ||
|
|
||
| it('should include backend type', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasBackend = true; | ||
| profile.backend.type = 'express'; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('Backend: express'); | ||
| }); | ||
|
|
||
| it('should include TypeScript', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasTypeScript = true; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain('TypeScript'); | ||
| }); | ||
|
|
||
| it('should join multiple parts with pipe', () => { | ||
| const profile = detector._createEmptyProfile(); | ||
| profile.hasFrontend = true; | ||
| profile.frontend.framework = 'react'; | ||
| profile.hasDatabase = true; | ||
| profile.database.type = 'supabase'; | ||
| const summary = TechStackDetector.getSummary(profile); | ||
| expect(summary).toContain(' | '); | ||
| }); | ||
| }); | ||
|
|
||
| // ─── _loadPackageJson caching ───────────────────────────────── | ||
|
|
||
| describe('_loadPackageJson', () => { | ||
| it('should cache package.json after first load', async () => { | ||
| mockPaths({ 'package.json': true }); | ||
| fs.readJson.mockResolvedValue({ name: 'test' }); | ||
|
|
||
| await detector._loadPackageJson(); | ||
| await detector._loadPackageJson(); | ||
|
|
||
| // readJson should only be called once due to caching | ||
| expect(fs.readJson).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('should return null when package.json does not exist', async () => { | ||
| fs.pathExists.mockResolvedValue(false); | ||
| const result = await detector._loadPackageJson(); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null on parse error', async () => { | ||
| mockPaths({ 'package.json': true }); | ||
| fs.readJson.mockRejectedValue(new Error('parse error')); | ||
|
|
||
| const result = await detector._loadPackageJson(); | ||
| expect(result).toBeNull(); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The PR description claims 74 tests, but there are actually 73 'it' blocks in this test file. Update the PR description to reflect the accurate count of 73 tests.
| await detector._loadPackageJson(); | ||
| await detector._loadPackageJson(); | ||
|
|
||
| // readJson should only be called once due to caching | ||
| expect(fs.readJson).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
While this test verifies that readJson is called only once, it doesn't verify that the cached value is actually returned on the second call. Consider adding an assertion like: const result1 = await detector._loadPackageJson(); const result2 = await detector._loadPackageJson(); expect(result1).toBe(result2); to ensure the cache is functioning correctly.
| await detector._loadPackageJson(); | |
| await detector._loadPackageJson(); | |
| // readJson should only be called once due to caching | |
| expect(fs.readJson).toHaveBeenCalledTimes(1); | |
| const result1 = await detector._loadPackageJson(); | |
| const result2 = await detector._loadPackageJson(); | |
| // readJson should only be called once due to caching | |
| expect(fs.readJson).toHaveBeenCalledTimes(1); | |
| // Both calls should return the same cached object instance | |
| expect(result1).toBe(result2); |
| describe('_detectBackend', () => { | ||
| it('should detect express', async () => { | ||
| mockPackageJson({ express: '^4.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('express'); | ||
| }); | ||
|
|
||
| it('should detect fastify', async () => { | ||
| mockPackageJson({ fastify: '^4.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('fastify'); | ||
| }); | ||
|
|
||
| it('should detect nestjs', async () => { | ||
| mockPackageJson({ '@nestjs/core': '^10.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('nest'); | ||
| }); | ||
|
|
||
| it('should detect hono', async () => { | ||
| mockPackageJson({ hono: '^3.0.0' }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('hono'); | ||
| }); | ||
|
|
||
| it('should detect edge functions from supabase/functions directory', async () => { | ||
| mockPaths({ 'package.json': false, 'supabase/functions': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.hasBackend).toBe(true); | ||
| expect(profile.backend.type).toBe('edge-functions'); | ||
| }); | ||
|
|
||
| it('should detect API routes from api directory', async () => { | ||
| mockPaths({ 'package.json': false, 'api': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.backend.hasAPI).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect API routes from pages/api (Next.js)', async () => { | ||
| mockPaths({ 'package.json': false, 'pages/api': true }); | ||
|
|
||
| const profile = detector._createEmptyProfile(); | ||
| await detector._detectBackend(profile); | ||
| expect(profile.backend.hasAPI).toBe(true); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The backend detection implementation allows the backend.type to be overwritten when multiple backend frameworks are present (e.g., express and fastify both set the type without checking if it's already set). This differs from database detection which preserves the first match. Consider adding a test case that verifies the behavior when multiple backend frameworks are detected to document the expected behavior.
Add 44 tests covering TechStackDetector class: - Database detection: Supabase, Prisma, PostgreSQL, MongoDB, MySQL, SQLite - Frontend detection: React, Vue, Angular, Svelte, Next.js, Nuxt - Build tools: Vite, Webpack; Styling: Tailwind, styled-components, Emotion, SCSS - Component libraries: shadcn, MUI; Fallback from .jsx files in src - Backend detection: Express, Fastify, NestJS, Hono, edge functions, API routes - TypeScript and test framework detection - Applicable phase computation and confidence scoring - Static getSummary method and package.json caching
cc4d115 to
f6a06a6
Compare
|
Consolidated into #426 |
Summary
.aios-core/core/orchestration/tech-stack-detector.js(599 lines)Test Coverage Highlights
Closes #225
Test plan
npx jest tests/core/orchestration/tech-stack-detector.test.jscc @Pedrovaleriolopez
Summary by CodeRabbit