From 3e861c3bca74bc63fb97e15419c2fbbdd4053e97 Mon Sep 17 00:00:00 2001 From: Aytug Berk Sezer Date: Fri, 26 Sep 2025 11:40:51 +0300 Subject: [PATCH] chore(ci): expand pipeline and resolve lint issues --- .github/workflows/ci.yml | 112 ++++++++++++++------ python_backend/utils/response_utils.py | 20 +++- scripts/autotest.js | 40 +++++-- views/CodemapPreviewModal.tsx | 27 ++--- views/FileTreeView.tsx | 18 ++-- views/FolderBrowserView.tsx | 19 ++-- views/KanbanBoardView.tsx | 4 +- views/LocalExclusionsManagerView.tsx | 6 +- views/RefinedLocalExclusionsManagerView.tsx | 2 +- views/RefinedSelectionGroupsView.tsx | 4 +- views/SelectionGroupsView.tsx | 4 +- views/StunningFolderBrowserView.tsx | 59 +++++------ views/TaskStoryAssociationModal.tsx | 4 +- views/UserStoryListView.tsx | 8 +- 14 files changed, 206 insertions(+), 121 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7535569..c035999 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,54 +1,104 @@ -# .github/workflows/ci.yml - name: CI on: push: - branches: [ "main" ] + branches: + - "main" pull_request: - branches: [ "main" ] + branches: + - "main" + workflow_dispatch: + +env: + NODE_VERSION: "20.x" + PYTHON_VERSION: "3.11" + NEXT_TELEMETRY_DISABLED: "1" + CI: "true" jobs: - build-and-test: - name: Build and Test + backend-tests: + name: Backend Tests runs-on: ubuntu-latest - steps: - # 1) Check out repository code - - name: Check out code - uses: actions/checkout@v3 + - name: Check out repository + uses: actions/checkout@v4 - # 2) Set up Python - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: python_backend/requirements.txt - # 3) Install Python dependencies - - name: Install Python dependencies + - name: Install backend dependencies run: | - python -m pip install --upgrade pip - pip install -r python_backend/requirements.txt + python -m venv python_backend/venv + python_backend/venv/bin/pip install --upgrade pip + python_backend/venv/bin/pip install -r python_backend/requirements.txt - # 4) Run Python unit tests - - name: Run Backend Tests - run: | - python -m pytest -q python_backend/tests + - name: Run backend unit tests + run: python_backend/venv/bin/pytest -q python_backend/tests + + frontend-quality: + name: Frontend Quality Gate + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 - # 5) Set up Node.js - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "20.x" + node-version: ${{ env.NODE_VERSION }} + cache: npm - # 6) Install Node dependencies (CI-friendly) - - name: Install Node Dependencies + - name: Install frontend dependencies run: npm ci - # 7) Build the Next.js app - - name: Build Next.js + - name: Run ESLint + run: npm run lint + + - name: Type-check with TypeScript + run: npx tsc --noEmit + + - name: Build Next.js app run: npm run build - # 8) Run the auto-tests (Python + Next.js) - - name: Run AutoTests - run: node scripts/autotest.js + integration-tests: + name: Integration AutoTests + runs-on: ubuntu-latest + needs: + - backend-tests + - frontend-quality + timeout-minutes: 25 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: python_backend/requirements.txt + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install backend dependencies + run: | + python -m venv python_backend/venv + python_backend/venv/bin/pip install --upgrade pip + python_backend/venv/bin/pip install -r python_backend/requirements.txt + + - name: Install frontend dependencies + run: npm ci + + - name: Run end-to-end AutoTests + env: + PORT: 3010 + BACKEND_PORT: 5010 + run: npm run test diff --git a/python_backend/utils/response_utils.py b/python_backend/utils/response_utils.py index 4e04095..b0e24d0 100644 --- a/python_backend/utils/response_utils.py +++ b/python_backend/utils/response_utils.py @@ -12,6 +12,20 @@ def success_response(data=None, message=None, status_code=200): return jsonify(response), status_code def error_response(error, message="An error occurred", status_code=400): - """Generates a standardized error JSON response.""" - response = {"success": False, "error": error, "message": message} - return jsonify(response), status_code \ No newline at end of file + """Generates a standardized error JSON response. + + NOTE: Older controller code sometimes called ``error_response(err, 404)`` + assuming the second positional argument was the status code. To remain + backwards compatible we detect that usage and treat numeric ``message`` + values as the status code instead of the human readable message. + """ + + if isinstance(message, int) and status_code == 400: + status_code = message + message = None + + response = {"success": False, "error": error} + if message: + response["message"] = message + + return jsonify(response), status_code diff --git a/scripts/autotest.js b/scripts/autotest.js index 9002dc6..3d3da1e 100644 --- a/scripts/autotest.js +++ b/scripts/autotest.js @@ -191,14 +191,25 @@ function getPortConfig() { /* ... same as before ... */ } headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseDir, paths: ["non_existent_file_12345.txt"] }), }); - if (!resp.ok) throw new Error("Expected HTTP 200 but got " + resp.status); - const data = await resp.json(); - if (!data.success) throw new Error("Expected success=true in response"); - if (!Array.isArray(data.data) || data.data.length !== 1) throw new Error("Expected exactly one file result"); - // Check for the specific error message prefix + if (resp.status !== 404) { + throw new Error(`Expected HTTP 404 but got ${resp.status}`); + } + + let data = {}; + try { + data = await resp.json(); + } catch { + /* ignore parse errors */ + } + + if (data.success !== false) { + throw new Error("Expected success=false in response"); + } + const fileNotFoundPrefix = "File not found on server:"; - if (!data.data[0].content || !data.data[0].content.startsWith(fileNotFoundPrefix)) { - throw new Error(`Expected a '${fileNotFoundPrefix}' message, got: "${data.data[0].content?.substring(0,100)}..."`); + const errorMessage = typeof data.error === 'string' ? data.error : String(data.message ?? ''); + if (!errorMessage.startsWith(fileNotFoundPrefix)) { + throw new Error(`Expected a '${fileNotFoundPrefix}' message, got: "${errorMessage.substring(0, 100)}..."`); } }); @@ -231,8 +242,19 @@ function getPortConfig() { /* ... same as before ... */ } let found = false; for (const path of scriptUrls) { - const url = path.startsWith('http') ? path : `${FRONTEND_BASE_URL}${path}`; - const jsResp = await fetch(url); + let url; + try { + url = new URL(path, FRONTEND_BASE_URL).toString(); + } catch { + continue; // Skip malformed script URLs + } + + let jsResp; + try { + jsResp = await fetch(url); + } catch { + continue; + } if (!jsResp.ok) continue; const js = await jsResp.text(); if (js.includes('execCommand("copy"') || js.includes("execCommand('copy'")) { diff --git a/views/CodemapPreviewModal.tsx b/views/CodemapPreviewModal.tsx index 62862ea..910e385 100644 --- a/views/CodemapPreviewModal.tsx +++ b/views/CodemapPreviewModal.tsx @@ -118,8 +118,11 @@ export default function CodemapPreviewModal({}: Props) { const [hoveredRowIndex, setHoveredRowIndex] = useState(null); const [viewMode, setViewMode] = useState("overview"); const [selectedFileType, setSelectedFileType] = useState("all"); - - const dataToDisplay = codemapModalData || {}; + + const dataToDisplay = useMemo>( + () => codemapModalData ?? {}, + [codemapModalData] + ); // Enhanced analytics and metrics const analytics = useMemo(() => { @@ -159,19 +162,19 @@ export default function CodemapPreviewModal({}: Props) { if (info.imports) { moduleGraph[file] = new Set(); info.imports.forEach(imp => { - const module = imp.module; - moduleGraph[file].add(module); - - if (!reverseGraph[module]) { - reverseGraph[module] = new Set(); + const moduleName = imp.module; + moduleGraph[file].add(moduleName); + + if (!reverseGraph[moduleName]) { + reverseGraph[moduleName] = new Set(); } - reverseGraph[module].add(file); - + reverseGraph[moduleName].add(file); + // Classify as internal or external - if (module.startsWith('.') || module.startsWith('/') || files.some(f => f.includes(module))) { - internalModules.add(module); + if (moduleName.startsWith('.') || moduleName.startsWith('/') || files.some(f => f.includes(moduleName))) { + internalModules.add(moduleName); } else { - externalModules.add(module); + externalModules.add(moduleName); } }); } diff --git a/views/FileTreeView.tsx b/views/FileTreeView.tsx index 9cc51d6..0d4c6de 100644 --- a/views/FileTreeView.tsx +++ b/views/FileTreeView.tsx @@ -25,7 +25,7 @@ import { GitBranch, Code, Database, - Image, + Image as ImageIcon, FileText, Package, } from "lucide-react"; @@ -174,10 +174,10 @@ const FileTreeView = forwardRef( yaml: , md: , txt: , - png: , - jpg: , - jpeg: , - svg: , + png: , + jpg: , + jpeg: , + svg: , git: , lock: , }; @@ -215,11 +215,9 @@ const FileTreeView = forwardRef( const isOpen = isDir && !collapsed.has(node.absolutePath); const rowId = node.absolutePath; - const selectableFiles = useMemo(() => { - const origin = findNodeByPath(fullTree, node.relativePath) ?? node; - return collectFileDescendants(origin); - }, [node, fullTree]); - + const origin = findNodeByPath(fullTree, node.relativePath) ?? node; + const selectableFiles = collectFileDescendants(origin); + const isEmpty = selectableFiles.length === 0; const checked = !isEmpty && selectableFiles.every((p) => selectedSet.has(p)); const partial = !isEmpty && !checked && selectableFiles.some((p) => selectedSet.has(p)); diff --git a/views/FolderBrowserView.tsx b/views/FolderBrowserView.tsx index b17a660..e7d8025 100644 --- a/views/FolderBrowserView.tsx +++ b/views/FolderBrowserView.tsx @@ -1,7 +1,7 @@ // views/FolderBrowserView.tsx // FIX: Clear search input when browsing into a new folder. -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ChevronLeft, Folder, @@ -77,21 +77,20 @@ export default function FolderBrowserView({ if (!isOpen) return; void loadDrives(); if (currentPath) void browse(currentPath); - // eslint‑disable‑next‑line react-hooks/exhaustive-deps - }, [isOpen]); + }, [isOpen, currentPath, loadDrives, browse]); /* -------------- helpers --------------- */ - async function fetchJson(url: string) { + const fetchJson = useCallback(async (url: string) => { const r = await fetch(url); if (!r.ok) { const msg = `${r.status} ${r.statusText}`; throw new Error(msg); } return r.json(); - } + }, []); /* -------------- API calls -------------- */ - async function loadDrives() { + const loadDrives = useCallback(async () => { try { setLoading(true); const raw = await fetchJson(`${API}/api/select_drives`); @@ -103,9 +102,9 @@ export default function FolderBrowserView({ } finally { setLoading(false); } - } + }, [fetchJson]); - async function browse(dir: string) { + const browse = useCallback(async (dir: string) => { try { setLoading(true); setSearch(''); // <<< FIX: Clear search term when browsing @@ -125,7 +124,7 @@ export default function FolderBrowserView({ } finally { setLoading(false); } - } + }, [fetchJson]); /* -------------- derived --------------- */ // Filter folders based on the search state @@ -263,4 +262,4 @@ export default function FolderBrowserView({ ); -} \ No newline at end of file +} diff --git a/views/KanbanBoardView.tsx b/views/KanbanBoardView.tsx index ff6dd50..8e9eeab 100644 --- a/views/KanbanBoardView.tsx +++ b/views/KanbanBoardView.tsx @@ -712,7 +712,7 @@ const KanbanBoardView: React.FC = () => { Active filters: {searchTerm && ( - Search: "{searchTerm}" + Search: “{searchTerm}” -

Delete group "{name}"

+

Delete group “{name}”

@@ -263,4 +263,4 @@ const RefinedSelectionGroupsView: React.FC = ({ ); }; -export default RefinedSelectionGroupsView; \ No newline at end of file +export default RefinedSelectionGroupsView; diff --git a/views/SelectionGroupsView.tsx b/views/SelectionGroupsView.tsx index 4807f39..5fec075 100644 --- a/views/SelectionGroupsView.tsx +++ b/views/SelectionGroupsView.tsx @@ -249,7 +249,7 @@ const SelectionGroupsView: React.FC = ({ -

Delete group "{name}"

+

Delete group “{name}”

@@ -275,4 +275,4 @@ const SelectionGroupsView: React.FC = ({ ); }; -export default SelectionGroupsView; \ No newline at end of file +export default SelectionGroupsView; diff --git a/views/StunningFolderBrowserView.tsx b/views/StunningFolderBrowserView.tsx index 5c9e9c6..f5969c0 100644 --- a/views/StunningFolderBrowserView.tsx +++ b/views/StunningFolderBrowserView.tsx @@ -1,7 +1,7 @@ // views/StunningFolderBrowserView.tsx // Behance-worthy folder browser with modern design -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ChevronLeft, Folder, @@ -66,54 +66,53 @@ export default function StunningFolderBrowserView({ // API base URL const API = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:5000'; - // Load drives on mount - useEffect(() => { - if (isOpen) { - loadDrives(); - if (currentPath) { - setPath(currentPath); - browse(currentPath); + // API helpers + const browse = useCallback(async (newPath: string) => { + setLoading(true); + setSearch(''); // Clear search when browsing + try { + const response = await fetch(`${API}/api/browse_folders?path=${encodeURIComponent(newPath)}`); + if (response.ok) { + const data = await response.json(); + setFolders(data.folders || []); + setPath(data.current_path || newPath); } + } catch (error) { + console.error('Failed to browse folder:', error); + } finally { + setLoading(false); } - }, [isOpen, currentPath]); + }, [API]); - const loadDrives = async () => { + const loadDrives = useCallback(async () => { try { const response = await fetch(`${API}/api/select_drives`); if (response.ok) { const data = await response.json(); const drives = Array.isArray(data) ? data : data.drives || []; - const normalizedDrives = drives.map((drive: any) => - typeof drive === 'string' + const normalizedDrives = drives.map((drive: any) => + typeof drive === 'string' ? { name: drive, path: drive } : { name: drive.name || drive.path, path: drive.path || drive.name } ); setDrives(normalizedDrives); if (!path && normalizedDrives.length > 0) { - browse(normalizedDrives[0].path); + void browse(normalizedDrives[0].path); } } } catch (error) { console.error('Failed to load drives:', error); } - }; + }, [API, browse, path]); - const browse = async (newPath: string) => { - setLoading(true); - setSearch(''); // Clear search when browsing - try { - const response = await fetch(`${API}/api/browse_folders?path=${encodeURIComponent(newPath)}`); - if (response.ok) { - const data = await response.json(); - setFolders(data.folders || []); - setPath(data.current_path || newPath); - } - } catch (error) { - console.error('Failed to browse folder:', error); - } finally { - setLoading(false); + useEffect(() => { + if (!isOpen) return; + void loadDrives(); + if (currentPath) { + setPath(currentPath); + void browse(currentPath); } - }; + }, [isOpen, currentPath, loadDrives, browse]); const navigateUp = () => { const separator = path.includes('\\') ? '\\' : '/'; @@ -389,4 +388,4 @@ export default function StunningFolderBrowserView({ ); -} \ No newline at end of file +} diff --git a/views/TaskStoryAssociationModal.tsx b/views/TaskStoryAssociationModal.tsx index d210f03..672dcc9 100644 --- a/views/TaskStoryAssociationModal.tsx +++ b/views/TaskStoryAssociationModal.tsx @@ -146,7 +146,7 @@ const TaskStoryAssociationModal: React.FC = ({ Manage Story Associations

- Link "{task.title}" to user stories + Link “{task.title}” to user stories

@@ -344,4 +344,4 @@ const TaskStoryAssociationModal: React.FC = ({ ); }; -export default TaskStoryAssociationModal; \ No newline at end of file +export default TaskStoryAssociationModal; diff --git a/views/UserStoryListView.tsx b/views/UserStoryListView.tsx index 28ef05f..b0e7414 100644 --- a/views/UserStoryListView.tsx +++ b/views/UserStoryListView.tsx @@ -353,7 +353,7 @@ const UserStoryListView: React.FC = () => { Active filters: {searchTerm && ( - Search: "{searchTerm}" + Search: “{searchTerm}” @@ -389,7 +389,7 @@ const UserStoryListView: React.FC = () => {

{searchTerm || filterPriority || filterStatus ? 'No stories match your current filters.' - : 'Start by adding a new user story using the "Add Story" button above.'} + : 'Start by adding a new user story using the “Add Story” button above.'}

) : ( @@ -554,7 +554,7 @@ const UserStoryListView: React.FC = () => { - Tasks for Story: "{storyTaskAssociation.title}" + Tasks for Story: “{storyTaskAssociation.title}”
@@ -649,4 +649,4 @@ const UserStoryListView: React.FC = () => { ); }; -export default UserStoryListView; \ No newline at end of file +export default UserStoryListView;