Skip to content

Commit 633e1e3

Browse files
committed
feat: refactor Python testing signal detection and improve dependency handling
1 parent 1660242 commit 633e1e3

File tree

8 files changed

+116
-124
lines changed

8 files changed

+116
-124
lines changed

app/api/scan-repo/route.ts

Lines changed: 4 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import type {
55
RepoScanResponse,
66
RepoScanSummary,
77
RepoStructureSummary,
8+
GitHubTreeItem,
9+
PackageJson,
810
} from "@/types/repo-scan"
911
import { buildDependencyAnalysisTasks, hasDependencyDetectionRules } from "@/lib/stack-detection"
1012
import type { DependencyAnalysisTask } from "@/lib/stack-detection"
11-
import { loadStackQuestionMetadata, normalizeConventionValue } from "@/lib/question-metadata"
1213
import { loadStackConventions } from "@/lib/conventions"
14+
import { dependencyHas } from "@/lib/repo-scan/dependency-utils"
15+
import { detectPythonTestingSignals } from "@/lib/repo-scan/python-testing-signals"
1316
import { inferStackFromScan } from "@/lib/scan-to-wizard"
1417
import { stackQuestion } from "@/lib/wizard-config"
1518

@@ -20,33 +23,6 @@ const JSON_HEADERS = {
2023
Accept: "application/vnd.github+json",
2124
}
2225

23-
interface GitHubTreeItem {
24-
path: string
25-
type: "blob" | "tree" | string
26-
}
27-
28-
interface PackageJson {
29-
dependencies?: Record<string, string>
30-
devDependencies?: Record<string, string>
31-
peerDependencies?: Record<string, string>
32-
optionalDependencies?: Record<string, string>
33-
engines?: { node?: string }
34-
workspaces?: string[] | { packages?: string[] }
35-
}
36-
37-
const dependencyHas = (pkg: PackageJson, names: string[]): boolean => {
38-
const sources = [
39-
pkg.dependencies,
40-
pkg.devDependencies,
41-
pkg.peerDependencies,
42-
pkg.optionalDependencies,
43-
]
44-
45-
return sources.some((source) =>
46-
source ? names.some((name) => Object.prototype.hasOwnProperty.call(source, name)) : false,
47-
)
48-
}
49-
5026
const isNullishOrEmpty = (value: unknown): value is null | undefined | "" => value === null || value === undefined || value === ""
5127

5228
const extractRateLimitRemaining = (response: Response): number | null => {
@@ -292,79 +268,6 @@ const detectTooling = async (
292268
}
293269
}
294270

295-
type TestingConventionValues = {
296-
unit: string[]
297-
e2e: string[]
298-
}
299-
300-
const testingConventionCache = new Map<string, TestingConventionValues>()
301-
302-
const getTestingConventionValues = async (stackId: string): Promise<TestingConventionValues> => {
303-
const normalized = stackId.trim().toLowerCase()
304-
if (testingConventionCache.has(normalized)) {
305-
return testingConventionCache.get(normalized)!
306-
}
307-
308-
const metadata = await loadStackQuestionMetadata(normalized)
309-
const values: TestingConventionValues = {
310-
unit: metadata.answersByResponseKey.testingUT ?? [],
311-
e2e: metadata.answersByResponseKey.testingE2E ?? [],
312-
}
313-
testingConventionCache.set(normalized, values)
314-
return values
315-
}
316-
317-
const findConventionValue = (values: string[], target: string): string | null => {
318-
const normalizedTarget = normalizeConventionValue(target)
319-
return values.find((value) => normalizeConventionValue(value) === normalizedTarget) ?? null
320-
}
321-
322-
const BEHAVE_DEPENDENCIES = ["behave", "behave-django", "behave-webdriver"]
323-
324-
export const detectPythonTestingSignals = async (
325-
paths: string[],
326-
pkg: PackageJson | null,
327-
testing: Set<string>,
328-
): Promise<void> => {
329-
const { unit } = await getTestingConventionValues("python")
330-
if (unit.length === 0) {
331-
return
332-
}
333-
334-
const behaveValue = findConventionValue(unit, "behave")
335-
const unittestValue = findConventionValue(unit, "unittest")
336-
337-
if (!behaveValue && !unittestValue) {
338-
return
339-
}
340-
341-
const lowerCasePaths = paths.map((path) => path.toLowerCase())
342-
343-
if (behaveValue) {
344-
const hasFeaturesDir = lowerCasePaths.some((path) => path.startsWith("features/") || path.includes("/features/"))
345-
const hasStepsDir = lowerCasePaths.some((path) => path.includes("/steps/"))
346-
const hasEnvironment = lowerCasePaths.some((path) => path.endsWith("/environment.py") || path.endsWith("environment.py"))
347-
const hasDependency = pkg ? dependencyHas(pkg, BEHAVE_DEPENDENCIES) : false
348-
349-
if (hasDependency || (hasFeaturesDir && (hasStepsDir || hasEnvironment))) {
350-
testing.add(behaveValue)
351-
}
352-
}
353-
354-
if (unittestValue) {
355-
const hasUnitFiles = lowerCasePaths.some((path) => {
356-
if (!/(^|\/)(tests?|testcases|specs)\//.test(path)) {
357-
return false
358-
}
359-
return /(^|\/)(test_[^/]+|[^/]+_test)\.py$/.test(path)
360-
})
361-
362-
if (hasUnitFiles) {
363-
testing.add(unittestValue)
364-
}
365-
}
366-
}
367-
368271
const readPackageJson = async (
369272
owner: string,
370273
repo: string,

app/layout.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
import type { Metadata } from "next";
22
import { Suspense } from "react";
33
import Script from "next/script";
4-
import { Geist, Geist_Mono } from "next/font/google";
54
import "./globals.css";
65
import { ThemeProvider } from "@/components/theme-provider";
76
import { MixpanelInit } from "@/components/MixpanelInit";
87
import { SITE_URL } from "@/lib/site-metadata";
98

10-
const geistSans = Geist({
11-
variable: "--font-geist-sans",
12-
subsets: ["latin"],
13-
});
14-
15-
const geistMono = Geist_Mono({
16-
variable: "--font-geist-mono",
17-
subsets: ["latin"],
18-
});
19-
209
const siteUrl = SITE_URL;
2110
const siteTitle = "DevContext – AI Coding Guidelines & Repo Analyzer";
2211
const siteDescription =
@@ -155,9 +144,7 @@ export default function RootLayout({
155144
}>) {
156145
return (
157146
<html lang="en" suppressHydrationWarning className="dark">
158-
<body
159-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
160-
>
147+
<body className="antialiased font-sans">
161148
<Script
162149
id="structured-data"
163150
type="application/ld+json"

lib/__tests__/repo-scan-python-detection.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { describe, expect, it } from "vitest"
22

3-
import { detectPythonTestingSignals } from "@/app/api/scan-repo/route"
4-
5-
type PackageJson = {
6-
dependencies?: Record<string, string>
7-
devDependencies?: Record<string, string>
8-
peerDependencies?: Record<string, string>
9-
optionalDependencies?: Record<string, string>
10-
}
3+
import { detectPythonTestingSignals } from "@/lib/repo-scan/python-testing-signals"
4+
import type { PackageJson } from "@/types/repo-scan"
115

126
const createPkg = (deps: Partial<PackageJson>): PackageJson => ({ ...deps })
137

lib/repo-scan/dependency-utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { PackageJson } from "@/types/repo-scan"
2+
3+
export const dependencyHas = (pkg: PackageJson, names: string[]): boolean => {
4+
const sources = [
5+
pkg.dependencies,
6+
pkg.devDependencies,
7+
pkg.peerDependencies,
8+
pkg.optionalDependencies,
9+
]
10+
11+
return sources.some((source) =>
12+
source ? names.some((name) => Object.prototype.hasOwnProperty.call(source, name)) : false,
13+
)
14+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { loadStackQuestionMetadata, normalizeConventionValue } from "@/lib/question-metadata"
2+
import { dependencyHas } from "@/lib/repo-scan/dependency-utils"
3+
import type { PackageJson } from "@/types/repo-scan"
4+
5+
type TestingConventionValues = {
6+
unit: string[]
7+
e2e: string[]
8+
}
9+
10+
const testingConventionCache = new Map<string, TestingConventionValues>()
11+
12+
const getTestingConventionValues = async (stackId: string): Promise<TestingConventionValues> => {
13+
const normalized = stackId.trim().toLowerCase()
14+
if (testingConventionCache.has(normalized)) {
15+
return testingConventionCache.get(normalized)!
16+
}
17+
18+
const metadata = await loadStackQuestionMetadata(normalized)
19+
const values: TestingConventionValues = {
20+
unit: metadata.answersByResponseKey.testingUT ?? [],
21+
e2e: metadata.answersByResponseKey.testingE2E ?? [],
22+
}
23+
testingConventionCache.set(normalized, values)
24+
return values
25+
}
26+
27+
const findConventionValue = (values: string[], target: string): string | null => {
28+
const normalizedTarget = normalizeConventionValue(target)
29+
return values.find((value) => normalizeConventionValue(value) === normalizedTarget) ?? null
30+
}
31+
32+
const BEHAVE_DEPENDENCIES = ["behave", "behave-django", "behave-webdriver"]
33+
34+
export const detectPythonTestingSignals = async (
35+
paths: string[],
36+
pkg: PackageJson | null,
37+
testing: Set<string>,
38+
): Promise<void> => {
39+
const { unit } = await getTestingConventionValues("python")
40+
if (unit.length === 0) {
41+
return
42+
}
43+
44+
const behaveValue = findConventionValue(unit, "behave")
45+
const unittestValue = findConventionValue(unit, "unittest")
46+
47+
if (!behaveValue && !unittestValue) {
48+
return
49+
}
50+
51+
const lowerCasePaths = paths.map((path) => path.toLowerCase())
52+
53+
if (behaveValue) {
54+
const hasFeaturesDir = lowerCasePaths.some((path) => path.startsWith("features/") || path.includes("/features/"))
55+
const hasStepsDir = lowerCasePaths.some((path) => path.includes("/steps/"))
56+
const hasEnvironment = lowerCasePaths.some((path) => path.endsWith("/environment.py") || path.endsWith("environment.py"))
57+
const hasDependency = pkg ? dependencyHas(pkg, BEHAVE_DEPENDENCIES) : false
58+
59+
if (hasDependency || (hasFeaturesDir && (hasStepsDir || hasEnvironment))) {
60+
testing.add(behaveValue)
61+
}
62+
}
63+
64+
if (unittestValue) {
65+
const hasUnitFiles = lowerCasePaths.some((path) => {
66+
if (!/(^|\/)(tests?|testcases|specs)\//.test(path)) {
67+
return false
68+
}
69+
return /(^|\/)(test_[^/]+|[^/]+_test)\.py$/.test(path)
70+
})
71+
72+
if (hasUnitFiles) {
73+
testing.add(unittestValue)
74+
}
75+
}
76+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",
7-
"build": "next build --turbopack",
7+
"build": "next build",
88
"start": "next start",
99
"lint": "eslint",
1010
"test": "vitest",

types/repo-scan.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,17 @@ export type RepoScanResponse = RepoScanSummary | RepoScanErrorResponse
5757
export type RepoScanRouteParams = {
5858
repoUrl: string
5959
}
60+
61+
export type GitHubTreeItem = {
62+
path: string
63+
type: "blob" | "tree" | string
64+
}
65+
66+
export type PackageJson = {
67+
dependencies?: Record<string, string>
68+
devDependencies?: Record<string, string>
69+
peerDependencies?: Record<string, string>
70+
optionalDependencies?: Record<string, string>
71+
engines?: { node?: string }
72+
workspaces?: string[] | { packages?: string[] }
73+
}

types/stack-detection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export type StackDependencyFileDetection = {
3939
* Explicit paths that should be evaluated (useful for root-level files).
4040
*/
4141
paths?: string[]
42+
/**
43+
* Legacy support for single path detection.
44+
*/
45+
path?: string
4246
/**
4347
* Signals that should be evaluated against the file contents.
4448
*/

0 commit comments

Comments
 (0)