@@ -6,8 +6,9 @@ import type {
66 RepoScanSummary ,
77 RepoStructureSummary ,
88} from "@/types/repo-scan"
9- import { inferStackFromScan } from "@/lib/scan-to-wizard "
9+ import { collectConventionValues , normalizeConventionValue } from "@/lib/convention-values "
1010import { loadStackConventions } from "@/lib/conventions"
11+ import { inferStackFromScan } from "@/lib/scan-to-wizard"
1112
1213const GITHUB_API_BASE_URL = "https://api.github.com"
1314const GITHUB_HOSTNAMES = new Set ( [ "github.com" , "www.github.com" ] )
@@ -114,7 +115,10 @@ const detectStructure = (paths: string[]): RepoStructureSummary => {
114115 }
115116}
116117
117- const detectTooling = ( paths : string [ ] , pkg : PackageJson | null ) : { tooling : string [ ] ; testing : string [ ] ; frameworks : string [ ] } => {
118+ const detectTooling = async (
119+ paths : string [ ] ,
120+ pkg : PackageJson | null ,
121+ ) : Promise < { tooling : string [ ] ; testing : string [ ] ; frameworks : string [ ] } > => {
118122 const tooling = new Set < string > ( )
119123 const testing = new Set < string > ( )
120124 const frameworks = new Set < string > ( )
@@ -276,13 +280,88 @@ const detectTooling = (paths: string[], pkg: PackageJson | null): { tooling: str
276280 }
277281 }
278282
283+ await detectPythonTestingSignals ( paths , pkg , testing )
284+
279285 return {
280286 tooling : dedupeAndSort ( tooling ) ,
281287 testing : dedupeAndSort ( testing ) ,
282288 frameworks : dedupeAndSort ( frameworks ) ,
283289 }
284290}
285291
292+ type TestingConventionValues = {
293+ unit : string [ ]
294+ e2e : string [ ]
295+ }
296+
297+ const testingConventionCache = new Map < string , TestingConventionValues > ( )
298+
299+ const getTestingConventionValues = async ( stackId : string ) : Promise < TestingConventionValues > => {
300+ const normalized = stackId . trim ( ) . toLowerCase ( )
301+ if ( testingConventionCache . has ( normalized ) ) {
302+ return testingConventionCache . get ( normalized ) !
303+ }
304+
305+ const { conventions } = await loadStackConventions ( normalized )
306+ const values : TestingConventionValues = {
307+ unit : collectConventionValues ( conventions , "testingUT" ) ,
308+ e2e : collectConventionValues ( conventions , "testingE2E" ) ,
309+ }
310+ testingConventionCache . set ( normalized , values )
311+ return values
312+ }
313+
314+ const findConventionValue = ( values : string [ ] , target : string ) : string | null => {
315+ const normalizedTarget = normalizeConventionValue ( target )
316+ return values . find ( ( value ) => normalizeConventionValue ( value ) === normalizedTarget ) ?? null
317+ }
318+
319+ const BEHAVE_DEPENDENCIES = [ "behave" , "behave-django" , "behave-webdriver" ]
320+
321+ export const detectPythonTestingSignals = async (
322+ paths : string [ ] ,
323+ pkg : PackageJson | null ,
324+ testing : Set < string > ,
325+ ) : Promise < void > => {
326+ const { unit } = await getTestingConventionValues ( "python" )
327+ if ( unit . length === 0 ) {
328+ return
329+ }
330+
331+ const behaveValue = findConventionValue ( unit , "behave" )
332+ const unittestValue = findConventionValue ( unit , "unittest" )
333+
334+ if ( ! behaveValue && ! unittestValue ) {
335+ return
336+ }
337+
338+ const lowerCasePaths = paths . map ( ( path ) => path . toLowerCase ( ) )
339+
340+ if ( behaveValue ) {
341+ const hasFeaturesDir = lowerCasePaths . some ( ( path ) => path . startsWith ( "features/" ) || path . includes ( "/features/" ) )
342+ const hasStepsDir = lowerCasePaths . some ( ( path ) => path . includes ( "/steps/" ) )
343+ const hasEnvironment = lowerCasePaths . some ( ( path ) => path . endsWith ( "/environment.py" ) || path . endsWith ( "environment.py" ) )
344+ const hasDependency = pkg ? dependencyHas ( pkg , BEHAVE_DEPENDENCIES ) : false
345+
346+ if ( hasDependency || ( hasFeaturesDir && ( hasStepsDir || hasEnvironment ) ) ) {
347+ testing . add ( behaveValue )
348+ }
349+ }
350+
351+ if ( unittestValue ) {
352+ const hasUnitFiles = lowerCasePaths . some ( ( path ) => {
353+ if ( ! / ( ^ | \/ ) ( t e s t s ? | t e s t c a s e s | s p e c s ) \/ / . test ( path ) ) {
354+ return false
355+ }
356+ return / ( ^ | \/ ) ( t e s t _ [ ^ / ] + | [ ^ / ] + _ t e s t ) \. p y $ / . test ( path )
357+ } )
358+
359+ if ( hasUnitFiles ) {
360+ testing . add ( unittestValue )
361+ }
362+ }
363+ }
364+
286365const readPackageJson = async (
287366 owner : string ,
288367 repo : string ,
@@ -770,7 +849,7 @@ export async function GET(request: NextRequest): Promise<NextResponse<RepoScanRe
770849
771850 const packageJson = hasPackageJson ? await readPackageJson ( owner , repo , defaultBranch , headers ) : null
772851
773- const { tooling, testing, frameworks } = detectTooling ( paths , packageJson )
852+ const { tooling, testing, frameworks } = await detectTooling ( paths , packageJson )
774853
775854 if ( lowestRateLimit !== null && lowestRateLimit < 5 ) {
776855 warnings . push ( `GitHub API rate limit is low (remaining: ${ lowestRateLimit } ).` )
0 commit comments