From 9702f142c4b742569ea40dca9c65f5814be38cfd Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 02:33:39 -0500 Subject: [PATCH 1/4] chore: update build scripts in package.json for improved package management - Modified the build command to first execute the build:packages script, ensuring all necessary packages are built before the UI. - Streamlined the build:packages command by consolidating workspace flags for better readability and maintenance. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bf80c93c..7022ef9a 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "dev:electron:wsl:gpu": "npm run dev:electron:wsl:gpu --workspace=apps/ui", "dev:server": "npm run dev --workspace=apps/server", "dev:full": "concurrently \"npm run dev:server\" \"npm run dev:web\"", - "build": "npm run build --workspace=apps/ui", - "build:packages": "npm run build -w @automaker/types && npm run build -w @automaker/platform && npm run build -w @automaker/utils && npm run build -w @automaker/prompts -w @automaker/model-resolver -w @automaker/dependency-resolver && npm run build -w @automaker/git-utils", + "build": "npm run build:packages && npm run build --workspace=apps/ui", + "build:packages": "npm run build -w @automaker/types -w @automaker/platform -w @automaker/utils -w @automaker/prompts -w @automaker/model-resolver -w @automaker/dependency-resolver -w @automaker/git-utils", "build:server": "npm run build --workspace=apps/server", "build:electron": "npm run build:electron --workspace=apps/ui", "build:electron:dir": "npm run build:electron:dir --workspace=apps/ui", From 9586589453283dd7fb2585f1dc9dec05453aeddd Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 12:10:54 -0500 Subject: [PATCH 2/4] fixing auto verify for kanban issues --- .github/workflows/security-audit.yml | 30 ++ apps/server/src/index.ts | 2 + apps/server/src/routes/github/index.ts | 18 + .../github/routes/check-github-remote.ts | 71 +++ .../server/src/routes/github/routes/common.ts | 35 ++ .../src/routes/github/routes/list-issues.ts | 89 ++++ .../src/routes/github/routes/list-prs.ts | 93 ++++ .../suggestions/generate-suggestions.ts | 12 +- apps/server/src/services/auto-mode-service.ts | 76 +++- .../sidebar/components/sidebar-navigation.tsx | 4 +- .../layout/sidebar/hooks/use-navigation.ts | 64 ++- .../sidebar/hooks/use-project-creation.ts | 22 +- .../ui/src/components/views/analysis-view.tsx | 82 ---- apps/ui/src/components/views/board-view.tsx | 4 +- .../kanban-card/card-content-sections.tsx | 33 +- .../components/kanban-card/kanban-card.tsx | 6 +- .../board-view/dialogs/add-feature-dialog.tsx | 6 - .../dialogs/edit-feature-dialog.tsx | 4 - .../dialogs/feature-suggestions-dialog.tsx | 21 +- .../board-view/hooks/use-board-actions.ts | 2 - .../board-view/shared/testing-tab-content.tsx | 50 +-- .../components/views/github-issues-view.tsx | 334 ++++++++++++++ .../src/components/views/github-prs-view.tsx | 421 ++++++++++++++++++ .../src/components/views/interview-view.tsx | 5 - apps/ui/src/lib/electron.ts | 174 +++++--- apps/ui/src/lib/http-api-client.ts | 10 + apps/ui/src/routes/github-issues.tsx | 6 + apps/ui/src/routes/github-prs.tsx | 6 + apps/ui/src/store/app-store.ts | 1 - libs/types/src/feature.ts | 1 - 30 files changed, 1376 insertions(+), 306 deletions(-) create mode 100644 .github/workflows/security-audit.yml create mode 100644 apps/server/src/routes/github/index.ts create mode 100644 apps/server/src/routes/github/routes/check-github-remote.ts create mode 100644 apps/server/src/routes/github/routes/common.ts create mode 100644 apps/server/src/routes/github/routes/list-issues.ts create mode 100644 apps/server/src/routes/github/routes/list-prs.ts create mode 100644 apps/ui/src/components/views/github-issues-view.tsx create mode 100644 apps/ui/src/components/views/github-prs-view.tsx create mode 100644 apps/ui/src/routes/github-issues.tsx create mode 100644 apps/ui/src/routes/github-prs.tsx diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 00000000..1a867179 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,30 @@ +name: Security Audit + +on: + pull_request: + branches: + - '*' + push: + branches: + - main + - master + schedule: + # Run weekly on Mondays at 9 AM UTC + - cron: '0 9 * * 1' + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup project + uses: ./.github/actions/setup-project + with: + check-lockfile: 'true' + + - name: Run npm audit + run: npm audit --audit-level=moderate + continue-on-error: false diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4a19c4c7..b2e9f115 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -46,6 +46,7 @@ import { SettingsService } from './services/settings-service.js'; import { createSpecRegenerationRoutes } from './routes/app-spec/index.js'; import { createClaudeRoutes } from './routes/claude/index.js'; import { ClaudeUsageService } from './services/claude-usage-service.js'; +import { createGitHubRoutes } from './routes/github/index.js'; // Load environment variables dotenv.config(); @@ -145,6 +146,7 @@ app.use('/api/templates', createTemplatesRoutes()); app.use('/api/terminal', createTerminalRoutes()); app.use('/api/settings', createSettingsRoutes(settingsService)); app.use('/api/claude', createClaudeRoutes(claudeUsageService)); +app.use('/api/github', createGitHubRoutes()); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/routes/github/index.ts b/apps/server/src/routes/github/index.ts new file mode 100644 index 00000000..bda4d217 --- /dev/null +++ b/apps/server/src/routes/github/index.ts @@ -0,0 +1,18 @@ +/** + * GitHub routes - HTTP API for GitHub integration + */ + +import { Router } from 'express'; +import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js'; +import { createListIssuesHandler } from './routes/list-issues.js'; +import { createListPRsHandler } from './routes/list-prs.js'; + +export function createGitHubRoutes(): Router { + const router = Router(); + + router.post('/check-remote', createCheckGitHubRemoteHandler()); + router.post('/issues', createListIssuesHandler()); + router.post('/prs', createListPRsHandler()); + + return router; +} diff --git a/apps/server/src/routes/github/routes/check-github-remote.ts b/apps/server/src/routes/github/routes/check-github-remote.ts new file mode 100644 index 00000000..34a07198 --- /dev/null +++ b/apps/server/src/routes/github/routes/check-github-remote.ts @@ -0,0 +1,71 @@ +/** + * GET /check-github-remote endpoint - Check if project has a GitHub remote + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; + +export interface GitHubRemoteStatus { + hasGitHubRemote: boolean; + remoteUrl: string | null; + owner: string | null; + repo: string | null; +} + +export async function checkGitHubRemote(projectPath: string): Promise { + const status: GitHubRemoteStatus = { + hasGitHubRemote: false, + remoteUrl: null, + owner: null, + repo: null, + }; + + try { + // Get the remote URL (origin by default) + const { stdout } = await execAsync('git remote get-url origin', { + cwd: projectPath, + env: execEnv, + }); + + const remoteUrl = stdout.trim(); + status.remoteUrl = remoteUrl; + + // Check if it's a GitHub URL + // Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git + const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/); + const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/); + + const match = httpsMatch || sshMatch; + if (match) { + status.hasGitHubRemote = true; + status.owner = match[1]; + status.repo = match[2].replace(/\.git$/, ''); + } + } catch { + // No remote or not a git repo - that's okay + } + + return status; +} + +export function createCheckGitHubRemoteHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + const status = await checkGitHubRemote(projectPath); + res.json({ + success: true, + ...status, + }); + } catch (error) { + logError(error, 'Check GitHub remote failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/github/routes/common.ts b/apps/server/src/routes/github/routes/common.ts new file mode 100644 index 00000000..790f92c3 --- /dev/null +++ b/apps/server/src/routes/github/routes/common.ts @@ -0,0 +1,35 @@ +/** + * Common utilities for GitHub routes + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; + +export const execAsync = promisify(exec); + +// Extended PATH to include common tool installation locations +export const extendedPath = [ + process.env.PATH, + '/opt/homebrew/bin', + '/usr/local/bin', + '/home/linuxbrew/.linuxbrew/bin', + `${process.env.HOME}/.local/bin`, +] + .filter(Boolean) + .join(':'); + +export const execEnv = { + ...process.env, + PATH: extendedPath, +}; + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +export function logError(error: unknown, context: string): void { + console.error(`[GitHub] ${context}:`, error); +} diff --git a/apps/server/src/routes/github/routes/list-issues.ts b/apps/server/src/routes/github/routes/list-issues.ts new file mode 100644 index 00000000..49850242 --- /dev/null +++ b/apps/server/src/routes/github/routes/list-issues.ts @@ -0,0 +1,89 @@ +/** + * POST /list-issues endpoint - List GitHub issues for a project + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; +import { checkGitHubRemote } from './check-github-remote.js'; + +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubIssue { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + body: string; +} + +export interface ListIssuesResult { + success: boolean; + issues?: GitHubIssue[]; + openIssues?: GitHubIssue[]; + closedIssues?: GitHubIssue[]; + error?: string; +} + +export function createListIssuesHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + // First check if this is a GitHub repo + const remoteStatus = await checkGitHubRemote(projectPath); + if (!remoteStatus.hasGitHubRemote) { + res.status(400).json({ + success: false, + error: 'Project does not have a GitHub remote', + }); + return; + } + + // Fetch open issues + const { stdout: openStdout } = await execAsync( + 'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ); + + // Fetch closed issues + const { stdout: closedStdout } = await execAsync( + 'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ); + + const openIssues: GitHubIssue[] = JSON.parse(openStdout || '[]'); + const closedIssues: GitHubIssue[] = JSON.parse(closedStdout || '[]'); + + res.json({ + success: true, + openIssues, + closedIssues, + issues: [...openIssues, ...closedIssues], + }); + } catch (error) { + logError(error, 'List GitHub issues failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/github/routes/list-prs.ts b/apps/server/src/routes/github/routes/list-prs.ts new file mode 100644 index 00000000..2dd0b2c4 --- /dev/null +++ b/apps/server/src/routes/github/routes/list-prs.ts @@ -0,0 +1,93 @@ +/** + * POST /list-prs endpoint - List GitHub pull requests for a project + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; +import { checkGitHubRemote } from './check-github-remote.js'; + +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubPR { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + isDraft: boolean; + headRefName: string; + reviewDecision: string | null; + mergeable: string; + body: string; +} + +export interface ListPRsResult { + success: boolean; + prs?: GitHubPR[]; + openPRs?: GitHubPR[]; + mergedPRs?: GitHubPR[]; + error?: string; +} + +export function createListPRsHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + // First check if this is a GitHub repo + const remoteStatus = await checkGitHubRemote(projectPath); + if (!remoteStatus.hasGitHubRemote) { + res.status(400).json({ + success: false, + error: 'Project does not have a GitHub remote', + }); + return; + } + + // Fetch open PRs + const { stdout: openStdout } = await execAsync( + 'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ); + + // Fetch merged PRs + const { stdout: mergedStdout } = await execAsync( + 'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ); + + const openPRs: GitHubPR[] = JSON.parse(openStdout || '[]'); + const mergedPRs: GitHubPR[] = JSON.parse(mergedStdout || '[]'); + + res.json({ + success: true, + openPRs, + mergedPRs, + prs: [...openPRs, ...mergedPRs], + }); + } catch (error) { + logError(error, 'List GitHub PRs failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 42514a0a..c8000ce5 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -23,10 +23,6 @@ const suggestionsSchema = { id: { type: 'string' }, category: { type: 'string' }, description: { type: 'string' }, - steps: { - type: 'array', - items: { type: 'string' }, - }, priority: { type: 'number', minimum: 1, @@ -34,7 +30,7 @@ const suggestionsSchema = { }, reasoning: { type: 'string' }, }, - required: ['category', 'description', 'steps', 'priority', 'reasoning'], + required: ['category', 'description', 'priority', 'reasoning'], }, }, }, @@ -62,9 +58,8 @@ Look at the codebase and provide 3-5 concrete suggestions. For each suggestion, provide: 1. A category (e.g., "User Experience", "Security", "Performance") 2. A clear description of what to implement -3. Concrete steps to implement it -4. Priority (1=high, 2=medium, 3=low) -5. Brief reasoning for why this would help +3. Priority (1=high, 2=medium, 3=low) +4. Brief reasoning for why this would help The response will be automatically formatted as structured JSON.`; @@ -164,7 +159,6 @@ The response will be automatically formatted as structured JSON.`; id: `suggestion-${Date.now()}-0`, category: 'Analysis', description: 'Review the AI analysis output for insights', - steps: ['Review the generated analysis'], priority: 1, reasoning: 'The AI provided analysis but suggestions need manual review', }, diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index da48308e..6621ff8a 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -599,15 +599,18 @@ export class AutoModeService { } ); - // Mark as waiting_approval for user review - await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval'); + // Determine final status based on testing mode: + // - skipTests=false (automated testing): go directly to 'verified' (no manual verify needed) + // - skipTests=true (manual verification): go to 'waiting_approval' for manual review + const finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; + await this.updateFeatureStatus(projectPath, featureId, finalStatus); this.emitAutoModeEvent('auto_mode_feature_complete', { featureId, passes: true, message: `Feature completed in ${Math.round( (Date.now() - tempRunningFeature.startTime) / 1000 - )}s`, + )}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`, projectPath, }); } catch (error) { @@ -868,13 +871,16 @@ Address the follow-up instructions above. Review the previous work and make the } ); - // Mark as waiting_approval for user review - await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval'); + // Determine final status based on testing mode: + // - skipTests=false (automated testing): go directly to 'verified' (no manual verify needed) + // - skipTests=true (manual verification): go to 'waiting_approval' for manual review + const finalStatus = feature?.skipTests ? 'waiting_approval' : 'verified'; + await this.updateFeatureStatus(projectPath, featureId, finalStatus); this.emitAutoModeEvent('auto_mode_feature_complete', { featureId, passes: true, - message: 'Follow-up completed successfully', + message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`, projectPath, }); } catch (error) { @@ -1652,15 +1658,17 @@ You can use the Read tool to view these images at any time during implementation `; } - prompt += ` + // Add verification instructions based on testing mode + if (feature.skipTests) { + // Manual verification - just implement the feature + prompt += ` ## Instructions Implement this feature by: 1. First, explore the codebase to understand the existing structure 2. Plan your implementation approach 3. Write the necessary code changes -4. Add or update tests as needed -5. Ensure the code follows existing patterns and conventions +4. Ensure the code follows existing patterns and conventions When done, wrap your final summary in tags like this: @@ -1678,6 +1686,56 @@ When done, wrap your final summary in tags like this: This helps parse your summary correctly in the output logs.`; + } else { + // Automated testing - implement and verify with Playwright + prompt += ` +## Instructions + +Implement this feature by: +1. First, explore the codebase to understand the existing structure +2. Plan your implementation approach +3. Write the necessary code changes +4. Ensure the code follows existing patterns and conventions + +## Verification with Playwright (REQUIRED) + +After implementing the feature, you MUST verify it works correctly using Playwright: + +1. **Create a temporary Playwright test** to verify the feature works as expected +2. **Run the test** to confirm the feature is working +3. **Delete the test file** after verification - this is a temporary verification test, not a permanent test suite addition + +Example verification workflow: +\`\`\`bash +# Create a simple verification test +npx playwright test my-verification-test.spec.ts + +# After successful verification, delete the test +rm my-verification-test.spec.ts +\`\`\` + +The test should verify the core functionality of the feature. If the test fails, fix the implementation and re-test. + +When done, wrap your final summary in tags like this: + + +## Summary: [Feature Title] + +### Changes Implemented +- [List of changes made] + +### Files Modified +- [List of files] + +### Verification Status +- [Describe how the feature was verified with Playwright] + +### Notes for Developer +- [Any important notes] + + +This helps parse your summary correctly in the output logs.`; + } return prompt; } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index 5178affa..b22dd8c1 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -51,7 +51,9 @@ export function SidebarNavigation({ return ( - - )} ); } diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx new file mode 100644 index 00000000..3d254cb2 --- /dev/null +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -0,0 +1,334 @@ +import { useState, useEffect, useCallback } from 'react'; +import { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react'; +import { getElectronAPI, GitHubIssue } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export function GitHubIssuesView() { + const [openIssues, setOpenIssues] = useState([]); + const [closedIssues, setClosedIssues] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [selectedIssue, setSelectedIssue] = useState(null); + const { currentProject } = useAppStore(); + + const fetchIssues = useCallback(async () => { + if (!currentProject?.path) { + setError('No project selected'); + setLoading(false); + return; + } + + try { + setError(null); + const api = getElectronAPI(); + if (api.github) { + const result = await api.github.listIssues(currentProject.path); + if (result.success) { + setOpenIssues(result.openIssues || []); + setClosedIssues(result.closedIssues || []); + } else { + setError(result.error || 'Failed to fetch issues'); + } + } + } catch (err) { + console.error('[GitHubIssuesView] Error fetching issues:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch issues'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [currentProject?.path]); + + useEffect(() => { + fetchIssues(); + }, [fetchIssues]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchIssues(); + }, [fetchIssues]); + + const handleOpenInGitHub = useCallback((url: string) => { + const api = getElectronAPI(); + api.openExternalLink(url); + }, []); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+ +
+

Failed to Load Issues

+

{error}

+ +
+ ); + } + + const totalIssues = openIssues.length + closedIssues.length; + + return ( +
+ {/* Issues List */} +
+ {/* Header */} +
+
+
+ +
+
+

Issues

+

+ {totalIssues === 0 + ? 'No issues found' + : `${openIssues.length} open, ${closedIssues.length} closed`} +

+
+
+ +
+ + {/* Issues List */} +
+ {totalIssues === 0 ? ( +
+
+ +
+

No Issues

+

This repository has no issues yet.

+
+ ) : ( +
+ {/* Open Issues */} + {openIssues.map((issue) => ( + setSelectedIssue(issue)} + onOpenExternal={() => handleOpenInGitHub(issue.url)} + formatDate={formatDate} + /> + ))} + + {/* Closed Issues Section */} + {closedIssues.length > 0 && ( + <> +
+ Closed Issues ({closedIssues.length}) +
+ {closedIssues.map((issue) => ( + setSelectedIssue(issue)} + onOpenExternal={() => handleOpenInGitHub(issue.url)} + formatDate={formatDate} + /> + ))} + + )} +
+ )} +
+
+ + {/* Issue Detail Panel */} + {selectedIssue && ( +
+ {/* Detail Header */} +
+
+ {selectedIssue.state === 'OPEN' ? ( + + ) : ( + + )} + + #{selectedIssue.number} {selectedIssue.title} + +
+
+ + +
+
+ + {/* Issue Detail Content */} +
+ {/* Title */} +

{selectedIssue.title}

+ + {/* Meta info */} +
+ + {selectedIssue.state === 'OPEN' ? 'Open' : 'Closed'} + + + #{selectedIssue.number} opened {formatDate(selectedIssue.createdAt)} by{' '} + {selectedIssue.author.login} + +
+ + {/* Labels */} + {selectedIssue.labels.length > 0 && ( +
+ {selectedIssue.labels.map((label) => ( + + {label.name} + + ))} +
+ )} + + {/* Body */} + {selectedIssue.body ? ( +
+
{selectedIssue.body}
+
+ ) : ( +

No description provided.

+ )} + + {/* Open in GitHub CTA */} +
+

+ View comments, add reactions, and more on GitHub. +

+ +
+
+
+ )} +
+ ); +} + +interface IssueRowProps { + issue: GitHubIssue; + isSelected: boolean; + onClick: () => void; + onOpenExternal: () => void; + formatDate: (date: string) => string; +} + +function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: IssueRowProps) { + return ( +
+ {issue.state === 'OPEN' ? ( + + ) : ( + + )} + +
+
+ {issue.title} +
+ +
+ + #{issue.number} opened {formatDate(issue.createdAt)} by {issue.author.login} + +
+ + {issue.labels.length > 0 && ( +
+ {issue.labels.map((label) => ( + + {label.name} + + ))} +
+ )} +
+ + +
+ ); +} diff --git a/apps/ui/src/components/views/github-prs-view.tsx b/apps/ui/src/components/views/github-prs-view.tsx new file mode 100644 index 00000000..4c613653 --- /dev/null +++ b/apps/ui/src/components/views/github-prs-view.tsx @@ -0,0 +1,421 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + GitPullRequest, + Loader2, + RefreshCw, + ExternalLink, + GitMerge, + Circle, + X, + AlertCircle, +} from 'lucide-react'; +import { getElectronAPI, GitHubPR } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export function GitHubPRsView() { + const [openPRs, setOpenPRs] = useState([]); + const [mergedPRs, setMergedPRs] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [selectedPR, setSelectedPR] = useState(null); + const { currentProject } = useAppStore(); + + const fetchPRs = useCallback(async () => { + if (!currentProject?.path) { + setError('No project selected'); + setLoading(false); + return; + } + + try { + setError(null); + const api = getElectronAPI(); + if (api.github) { + const result = await api.github.listPRs(currentProject.path); + if (result.success) { + setOpenPRs(result.openPRs || []); + setMergedPRs(result.mergedPRs || []); + } else { + setError(result.error || 'Failed to fetch pull requests'); + } + } + } catch (err) { + console.error('[GitHubPRsView] Error fetching PRs:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch pull requests'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [currentProject?.path]); + + useEffect(() => { + fetchPRs(); + }, [fetchPRs]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchPRs(); + }, [fetchPRs]); + + const handleOpenInGitHub = useCallback((url: string) => { + const api = getElectronAPI(); + api.openExternalLink(url); + }, []); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + const getReviewStatus = (pr: GitHubPR) => { + if (pr.isDraft) return { label: 'Draft', color: 'text-muted-foreground', bg: 'bg-muted' }; + switch (pr.reviewDecision) { + case 'APPROVED': + return { label: 'Approved', color: 'text-green-500', bg: 'bg-green-500/10' }; + case 'CHANGES_REQUESTED': + return { label: 'Changes requested', color: 'text-orange-500', bg: 'bg-orange-500/10' }; + case 'REVIEW_REQUIRED': + return { label: 'Review required', color: 'text-yellow-500', bg: 'bg-yellow-500/10' }; + default: + return null; + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+ +
+

Failed to Load Pull Requests

+

{error}

+ +
+ ); + } + + const totalPRs = openPRs.length + mergedPRs.length; + + return ( +
+ {/* PR List */} +
+ {/* Header */} +
+
+
+ +
+
+

Pull Requests

+

+ {totalPRs === 0 + ? 'No pull requests found' + : `${openPRs.length} open, ${mergedPRs.length} merged`} +

+
+
+ +
+ + {/* PR List */} +
+ {totalPRs === 0 ? ( +
+
+ +
+

No Pull Requests

+

+ This repository has no pull requests yet. +

+
+ ) : ( +
+ {/* Open PRs */} + {openPRs.map((pr) => ( + setSelectedPR(pr)} + onOpenExternal={() => handleOpenInGitHub(pr.url)} + formatDate={formatDate} + getReviewStatus={getReviewStatus} + /> + ))} + + {/* Merged PRs Section */} + {mergedPRs.length > 0 && ( + <> +
+ Merged ({mergedPRs.length}) +
+ {mergedPRs.map((pr) => ( + setSelectedPR(pr)} + onOpenExternal={() => handleOpenInGitHub(pr.url)} + formatDate={formatDate} + getReviewStatus={getReviewStatus} + /> + ))} + + )} +
+ )} +
+
+ + {/* PR Detail Panel */} + {selectedPR && ( +
+ {/* Detail Header */} +
+
+ {selectedPR.state === 'MERGED' ? ( + + ) : ( + + )} + + #{selectedPR.number} {selectedPR.title} + + {selectedPR.isDraft && ( + + Draft + + )} +
+
+ + +
+
+ + {/* PR Detail Content */} +
+ {/* Title */} +

{selectedPR.title}

+ + {/* Meta info */} +
+ + {selectedPR.state === 'MERGED' ? 'Merged' : selectedPR.isDraft ? 'Draft' : 'Open'} + + {getReviewStatus(selectedPR) && ( + + {getReviewStatus(selectedPR)!.label} + + )} + + #{selectedPR.number} opened {formatDate(selectedPR.createdAt)} by{' '} + {selectedPR.author.login} + +
+ + {/* Branch info */} + {selectedPR.headRefName && ( +
+ Branch: + + {selectedPR.headRefName} + +
+ )} + + {/* Labels */} + {selectedPR.labels.length > 0 && ( +
+ {selectedPR.labels.map((label) => ( + + {label.name} + + ))} +
+ )} + + {/* Body */} + {selectedPR.body ? ( +
+
{selectedPR.body}
+
+ ) : ( +

No description provided.

+ )} + + {/* Open in GitHub CTA */} +
+

+ View code changes, comments, and reviews on GitHub. +

+ +
+
+
+ )} +
+ ); +} + +interface PRRowProps { + pr: GitHubPR; + isSelected: boolean; + onClick: () => void; + onOpenExternal: () => void; + formatDate: (date: string) => string; + getReviewStatus: (pr: GitHubPR) => { label: string; color: string; bg: string } | null; +} + +function PRRow({ + pr, + isSelected, + onClick, + onOpenExternal, + formatDate, + getReviewStatus, +}: PRRowProps) { + const reviewStatus = getReviewStatus(pr); + + return ( +
+ {pr.state === 'MERGED' ? ( + + ) : ( + + )} + +
+
+ {pr.title} + {pr.isDraft && ( + + Draft + + )} +
+ +
+ + #{pr.number} opened {formatDate(pr.createdAt)} by {pr.author.login} + + {pr.headRefName && ( + + {pr.headRefName} + + )} +
+ +
+ {/* Review Status */} + {reviewStatus && ( + + {reviewStatus.label} + + )} + + {/* Labels */} + {pr.labels.map((label) => ( + + {label.name} + + ))} +
+
+ + +
+ ); +} diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx index 47127e10..71d0fa4d 100644 --- a/apps/ui/src/components/views/interview-view.tsx +++ b/apps/ui/src/components/views/interview-view.tsx @@ -345,11 +345,6 @@ export function InterviewView() { category: 'Core', description: 'Initial project setup', status: 'backlog' as const, - steps: [ - 'Step 1: Review app_spec.txt', - 'Step 2: Set up development environment', - 'Step 3: Start implementing features', - ], skipTests: true, }; diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 0cb4c4db..87bb61cd 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -92,12 +92,77 @@ export interface RunningAgentsAPI { getAll: () => Promise; } +// GitHub types +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubIssue { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + body: string; +} + +export interface GitHubPR { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + isDraft: boolean; + headRefName: string; + reviewDecision: string | null; + mergeable: string; + body: string; +} + +export interface GitHubRemoteStatus { + hasGitHubRemote: boolean; + remoteUrl: string | null; + owner: string | null; + repo: string | null; +} + +export interface GitHubAPI { + checkRemote: (projectPath: string) => Promise<{ + success: boolean; + hasGitHubRemote?: boolean; + remoteUrl?: string | null; + owner?: string | null; + repo?: string | null; + error?: string; + }>; + listIssues: (projectPath: string) => Promise<{ + success: boolean; + openIssues?: GitHubIssue[]; + closedIssues?: GitHubIssue[]; + error?: string; + }>; + listPRs: (projectPath: string) => Promise<{ + success: boolean; + openPRs?: GitHubPR[]; + mergedPRs?: GitHubPR[]; + error?: string; + }>; +} + // Feature Suggestions types export interface FeatureSuggestion { id: string; category: string; description: string; - steps: string[]; priority: number; reasoning: string; } @@ -326,6 +391,7 @@ export interface ElectronAPI { autoMode?: AutoModeAPI; features?: FeaturesAPI; runningAgents?: RunningAgentsAPI; + github?: GitHubAPI; enhancePrompt?: { enhance: ( originalText: string, @@ -861,6 +927,9 @@ const getMockElectronAPI = (): ElectronAPI => { // Mock Running Agents API runningAgents: createMockRunningAgentsAPI(), + // Mock GitHub API + github: createMockGitHubAPI(), + // Mock Claude API claude: { getUsage: async () => { @@ -1963,12 +2032,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'Code Smell', description: 'Extract duplicate validation logic into reusable utility', - steps: [ - 'Identify all files with similar validation patterns', - 'Create a validation utilities module', - 'Replace duplicate code with utility calls', - 'Add unit tests for the new utilities', - ], priority: 1, reasoning: 'Reduces code duplication and improves maintainability', }, @@ -1976,12 +2039,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Complexity', description: 'Break down large handleSubmit function into smaller functions', - steps: [ - 'Identify the handleSubmit function in form components', - 'Extract validation logic into separate function', - 'Extract API call logic into separate function', - 'Extract success/error handling into separate functions', - ], priority: 2, reasoning: 'Function is too long and handles multiple responsibilities', }, @@ -1989,12 +2046,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Architecture', description: 'Move business logic out of React components into hooks', - steps: [ - 'Identify business logic in component files', - 'Create custom hooks for reusable logic', - 'Update components to use the new hooks', - 'Add tests for the extracted hooks', - ], priority: 3, reasoning: 'Improves separation of concerns and testability', }, @@ -2007,12 +2058,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'High', description: 'Sanitize user input before rendering to prevent XSS', - steps: [ - 'Audit all places where user input is rendered', - 'Implement input sanitization using DOMPurify', - 'Add Content-Security-Policy headers', - 'Test with common XSS payloads', - ], priority: 1, reasoning: 'User input is rendered without proper sanitization', }, @@ -2020,12 +2065,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Medium', description: 'Add rate limiting to authentication endpoints', - steps: [ - 'Implement rate limiting middleware', - 'Configure limits for login attempts', - 'Add account lockout after failed attempts', - 'Log suspicious activity', - ], priority: 2, reasoning: 'Prevents brute force attacks on authentication', }, @@ -2033,12 +2072,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Low', description: 'Remove sensitive information from error messages', - steps: [ - 'Audit error handling in API routes', - 'Create generic error messages for production', - 'Log detailed errors server-side only', - 'Implement proper error boundaries', - ], priority: 3, reasoning: 'Error messages may leak implementation details', }, @@ -2051,12 +2084,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'Rendering', description: 'Add React.memo to prevent unnecessary re-renders', - steps: [ - 'Profile component renders with React DevTools', - 'Identify components that re-render unnecessarily', - 'Wrap pure components with React.memo', - 'Use useCallback for event handlers passed as props', - ], priority: 1, reasoning: "Components re-render even when props haven't changed", }, @@ -2064,12 +2091,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Bundle Size', description: 'Implement code splitting for route components', - steps: [ - 'Use React.lazy for route components', - 'Add Suspense boundaries with loading states', - 'Analyze bundle with webpack-bundle-analyzer', - 'Consider dynamic imports for heavy libraries', - ], priority: 2, reasoning: 'Initial bundle is larger than necessary', }, @@ -2077,12 +2098,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Caching', description: 'Add memoization for expensive computations', - steps: [ - 'Identify expensive calculations in render', - 'Use useMemo for derived data', - 'Consider using react-query for server state', - 'Add caching headers for static assets', - ], priority: 3, reasoning: 'Expensive computations run on every render', }, @@ -2095,12 +2110,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'User Experience', description: 'Add dark mode toggle with system preference detection', - steps: [ - 'Create a ThemeProvider context to manage theme state', - 'Add a toggle component in the settings or header', - 'Implement CSS variables for theme colors', - 'Add localStorage persistence for user preference', - ], priority: 1, reasoning: 'Dark mode is a standard feature that improves accessibility and user comfort', }, @@ -2108,11 +2117,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Performance', description: 'Implement lazy loading for heavy components', - steps: [ - 'Identify components that are heavy or rarely used', - 'Use React.lazy() and Suspense for code splitting', - 'Add loading states for lazy-loaded components', - ], priority: 2, reasoning: 'Improves initial load time and reduces bundle size', }, @@ -2120,12 +2124,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Accessibility', description: 'Add keyboard navigation support throughout the app', - steps: [ - 'Implement focus management for modals and dialogs', - 'Add keyboard shortcuts for common actions', - 'Ensure all interactive elements are focusable', - 'Add ARIA labels and roles where needed', - ], priority: 3, reasoning: 'Improves accessibility for users who rely on keyboard navigation', }, @@ -2592,6 +2590,38 @@ function createMockRunningAgentsAPI(): RunningAgentsAPI { }; } +// Mock GitHub API implementation +function createMockGitHubAPI(): GitHubAPI { + return { + checkRemote: async (projectPath: string) => { + console.log('[Mock] Checking GitHub remote for:', projectPath); + return { + success: true, + hasGitHubRemote: false, + remoteUrl: null, + owner: null, + repo: null, + }; + }, + listIssues: async (projectPath: string) => { + console.log('[Mock] Listing GitHub issues for:', projectPath); + return { + success: true, + openIssues: [], + closedIssues: [], + }; + }, + listPRs: async (projectPath: string) => { + console.log('[Mock] Listing GitHub PRs for:', projectPath); + return { + success: true, + openPRs: [], + mergedPRs: [], + }; + }, + }; +} + // Utility functions for project management export interface Project { diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index cdabf0e3..18e77fe4 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -21,6 +21,9 @@ import type { SuggestionsEvent, SpecRegenerationEvent, SuggestionType, + GitHubAPI, + GitHubIssue, + GitHubPR, } from './electron'; import type { Message, SessionListItem } from '@/types/electron'; import type { Feature, ClaudeUsageResponse } from '@/store/app-store'; @@ -743,6 +746,13 @@ export class HttpApiClient implements ElectronAPI { }> => this.get('/api/running-agents'), }; + // GitHub API + github: GitHubAPI = { + checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }), + listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }), + listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }), + }; + // Workspace API workspace = { getConfig: (): Promise<{ diff --git a/apps/ui/src/routes/github-issues.tsx b/apps/ui/src/routes/github-issues.tsx new file mode 100644 index 00000000..16506e47 --- /dev/null +++ b/apps/ui/src/routes/github-issues.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { GitHubIssuesView } from '@/components/views/github-issues-view'; + +export const Route = createFileRoute('/github-issues')({ + component: GitHubIssuesView, +}); diff --git a/apps/ui/src/routes/github-prs.tsx b/apps/ui/src/routes/github-prs.tsx new file mode 100644 index 00000000..78e321a6 --- /dev/null +++ b/apps/ui/src/routes/github-prs.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { GitHubPRsView } from '@/components/views/github-prs-view'; + +export const Route = createFileRoute('/github-prs')({ + component: GitHubPRsView, +}); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 8927617e..394160b7 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -266,7 +266,6 @@ export interface Feature { titleGenerating?: boolean; category: string; description: string; - steps: string[]; status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified' | 'completed'; images?: FeatureImage[]; imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context diff --git a/libs/types/src/feature.ts b/libs/types/src/feature.ts index a4946336..6aa2473b 100644 --- a/libs/types/src/feature.ts +++ b/libs/types/src/feature.ts @@ -18,7 +18,6 @@ export interface Feature { titleGenerating?: boolean; category: string; description: string; - steps?: string[]; passes?: boolean; priority?: number; status?: string; From 0c508ce1305f3da3e3b3f87c2d9bcb021a84b02d Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 12:49:48 -0500 Subject: [PATCH 3/4] feat: add end-to-end testing guide and project creation tests - Introduced a comprehensive E2E Testing Guide outlining best practices for Playwright tests, including principles for test isolation, element selection, and setup utilities. - Added new test files for project creation and opening existing projects, ensuring functionality for creating blank projects and projects from GitHub templates. - Implemented utility functions for setting up test states and managing localStorage, enhancing maintainability and reducing boilerplate in tests. --- apps/ui/tests/e2e-testing-guide.md | 306 +++++++++++++++++++++++++ apps/ui/tests/open-project.spec.ts | 125 ++++++++++ apps/ui/tests/project-creation.spec.ts | 188 +++++++++++++++ apps/ui/tests/utils/project/setup.ts | 165 ++++++++++++- 4 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 apps/ui/tests/e2e-testing-guide.md create mode 100644 apps/ui/tests/open-project.spec.ts create mode 100644 apps/ui/tests/project-creation.spec.ts diff --git a/apps/ui/tests/e2e-testing-guide.md b/apps/ui/tests/e2e-testing-guide.md new file mode 100644 index 00000000..8b07dd53 --- /dev/null +++ b/apps/ui/tests/e2e-testing-guide.md @@ -0,0 +1,306 @@ +# E2E Testing Guide + +Best practices and patterns for writing reliable, non-flaky Playwright e2e tests in this codebase. + +## Core Principles + +1. **No arbitrary timeouts** - Never use `page.waitForTimeout()`. Always wait for specific conditions. +2. **Use data-testid attributes** - Prefer `[data-testid="..."]` selectors over CSS classes or text content. +3. **Clean up after tests** - Use unique temp directories and clean them up in `afterAll`. +4. **Test isolation** - Each test should be independent and not rely on state from other tests. + +## Setting Up Test State + +### Use Setup Utilities (Recommended) + +Use the provided utility functions to set up localStorage state. These utilities hide the internal store structure and version details, making tests more maintainable. + +```typescript +import { setupWelcomeView, setupRealProject } from './utils'; + +// Show welcome view with workspace directory configured +await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR }); + +// Show welcome view with recent projects +await setupWelcomeView(page, { + workspaceDir: TEST_TEMP_DIR, + recentProjects: [ + { + id: 'project-123', + name: 'My Project', + path: '/path/to/project', + lastOpened: new Date().toISOString(), + }, + ], +}); + +// Set up a real project on the filesystem +await setupRealProject(page, projectPath, projectName, { + setAsCurrent: true, // Opens board view (default) +}); +``` + +### Why Use Utilities Instead of Raw localStorage + +1. **Version management** - Store versions are centralized in one place +2. **Less brittle** - If store structure changes, update one file instead of every test +3. **Cleaner tests** - Focus on test logic, not setup boilerplate +4. **Type safety** - Utilities provide typed interfaces for test data + +### Manual LocalStorage Setup (Advanced) + +If you need custom setup not covered by utilities, use `page.addInitScript()`. +Store versions are defined in `tests/utils/project/setup.ts`: + +- `APP_STORE`: version 2 (matches `app-store.ts`) +- `SETUP_STORE`: version 0 (matches `setup-store.ts` default) + +### Temp Directory Management + +Create unique temp directories for test isolation: + +```typescript +import { createTempDirPath, cleanupTempDir } from './utils'; + +const TEST_TEMP_DIR = createTempDirPath('my-test-name'); + +test.describe('My Tests', () => { + test.beforeAll(async () => { + if (!fs.existsSync(TEST_TEMP_DIR)) { + fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); + } + }); + + test.afterAll(async () => { + cleanupTempDir(TEST_TEMP_DIR); + }); +}); +``` + +## Waiting for Elements + +### Prefer `toBeVisible()` over `waitForSelector()` + +```typescript +// Good - uses Playwright's auto-waiting with expect +await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); + +// Avoid - manual waiting +await page.waitForSelector('[data-testid="welcome-view"]'); +``` + +### Wait for network idle after navigation + +```typescript +await page.goto('/'); +await page.waitForLoadState('networkidle'); +``` + +### Use appropriate timeouts + +- Quick UI updates: 5000ms (default) +- Page loads/navigation: 10000ms +- Async operations (API calls, file system): 15000ms + +```typescript +// Fast UI element +await expect(button).toBeVisible(); + +// Page load +await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 }); + +// Async operation completion +await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); +``` + +## Element Selection + +### Use data-testid attributes + +```typescript +// Good - stable selector +const button = page.locator('[data-testid="create-new-project"]'); + +// Avoid - brittle selectors +const button = page.locator('.btn-primary'); +const button = page.getByText('Create'); +``` + +### Scope selectors when needed + +When text appears in multiple places, scope to a parent: + +```typescript +// Bad - might match multiple elements +await expect(page.getByText(projectName)).toBeVisible(); + +// Good - scoped to specific container +await expect(page.locator('[data-testid="project-selector"]').getByText(projectName)).toBeVisible(); +``` + +### Handle strict mode violations + +If a selector matches multiple elements: + +```typescript +// Use .first() if you need the first match +await page.locator('[data-testid="item"]').first().click(); + +// Or scope to a unique parent +await page.locator('[data-testid="sidebar"]').locator('[data-testid="item"]').click(); +``` + +## Clicking Elements + +### Always verify visibility before clicking + +```typescript +const button = page.locator('[data-testid="submit"]'); +await expect(button).toBeVisible(); +await button.click(); +``` + +### Handle dialogs that may close quickly + +Some dialogs may appear briefly or auto-close. Don't rely on clicking them: + +```typescript +// Instead of trying to close a dialog that might disappear: +// await expect(dialog).toBeVisible(); +// await closeButton.click(); // May fail if dialog closes first + +// Just verify the end state: +await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); +``` + +## Filesystem Verification + +Verify files were created after async operations: + +```typescript +// Wait for UI to confirm operation completed first +await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); + +// Then verify filesystem +const projectPath = path.join(TEST_TEMP_DIR, projectName); +expect(fs.existsSync(projectPath)).toBe(true); + +const appSpecPath = path.join(projectPath, '.automaker', 'app_spec.txt'); +expect(fs.existsSync(appSpecPath)).toBe(true); + +const content = fs.readFileSync(appSpecPath, 'utf-8'); +expect(content).toContain(projectName); +``` + +## Test Structure + +### Use descriptive test names + +```typescript +test('should create a new blank project from welcome view', async ({ page }) => { + // ... +}); +``` + +### Group related tests with describe blocks + +```typescript +test.describe('Project Creation', () => { + test('should create a new blank project from welcome view', ...); + test('should create a project from template', ...); +}); +``` + +### Use serial mode when tests depend on each other + +```typescript +test.describe.configure({ mode: 'serial' }); +``` + +## Common Patterns + +### Waiting for either of two outcomes + +When multiple outcomes are possible (e.g., dialog or direct navigation): + +```typescript +// Wait for either the dialog or the board view +await Promise.race([ + initDialog.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}), + boardView.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}), +]); + +// Then handle whichever appeared +if (await initDialog.isVisible()) { + await closeButton.click(); +} + +await expect(boardView).toBeVisible(); +``` + +### Generating unique test data + +```typescript +const projectName = `test-project-${Date.now()}`; +``` + +## Running Tests + +```bash +# Run all tests +npm run test + +# Run specific test file +npm run test -- project-creation.spec.ts + +# Run with headed browser (see what's happening) +npm run test:headed -- project-creation.spec.ts + +# Run multiple times to check for flakiness +npm run test -- project-creation.spec.ts --repeat-each=5 +``` + +## Debugging Failed Tests + +1. Check the screenshot in `test-results/` +2. Read the error context markdown file in `test-results/` +3. Run with `--headed` to watch the test +4. Add `await page.pause()` to pause execution at a specific point + +## Available Test Utilities + +Import from `./utils`: + +### State Setup Utilities + +- `setupWelcomeView(page, options?)` - Set up empty state showing welcome view + - `options.workspaceDir` - Pre-configure workspace directory + - `options.recentProjects` - Add projects to recent list (not current) +- `setupRealProject(page, path, name, options?)` - Set up state with a real filesystem project + - `options.setAsCurrent` - Open board view (default: true) + - `options.additionalProjects` - Add more projects to list +- `setupMockProject(page)` - Set up mock project for unit-style tests +- `setupComplete(page)` - Mark setup wizard as complete + +### Filesystem Utilities + +- `createTempDirPath(prefix)` - Create unique temp directory path +- `cleanupTempDir(path)` - Remove temp directory +- `createTestGitRepo(tempDir)` - Create a git repo for testing + +### Waiting Utilities + +- `waitForNetworkIdle(page)` - Wait for network to be idle +- `waitForElement(page, testId)` - Wait for element by test ID + +### Async File Verification + +Use `expect().toPass()` for polling filesystem operations: + +```typescript +await expect(async () => { + expect(fs.existsSync(filePath)).toBe(true); +}).toPass({ timeout: 10000 }); +``` + +See `tests/utils/index.ts` for the full list of available utilities. diff --git a/apps/ui/tests/open-project.spec.ts b/apps/ui/tests/open-project.spec.ts new file mode 100644 index 00000000..87885604 --- /dev/null +++ b/apps/ui/tests/open-project.spec.ts @@ -0,0 +1,125 @@ +/** + * Open Project End-to-End Test + * + * Tests opening an existing project directory from the welcome view. + * This verifies that: + * 1. An existing directory can be opened as a project + * 2. The .automaker directory is initialized if it doesn't exist + * 3. The project is loaded and shown in the board view + */ + +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; +import { createTempDirPath, cleanupTempDir, setupWelcomeView } from './utils'; + +// Create unique temp dir for this test run +const TEST_TEMP_DIR = createTempDirPath('open-project-test'); + +test.describe('Open Project', () => { + test.beforeAll(async () => { + // Create test temp directory + if (!fs.existsSync(TEST_TEMP_DIR)) { + fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); + } + }); + + test.afterAll(async () => { + // Cleanup temp directory + cleanupTempDir(TEST_TEMP_DIR); + }); + + test('should open an existing project directory from recent projects', async ({ page }) => { + const projectName = `existing-project-${Date.now()}`; + const projectPath = path.join(TEST_TEMP_DIR, projectName); + const projectId = `project-${Date.now()}`; + + // Create the project directory with some files to simulate an existing codebase + fs.mkdirSync(projectPath, { recursive: true }); + + // Create a package.json to simulate a real project + fs.writeFileSync( + path.join(projectPath, 'package.json'), + JSON.stringify( + { + name: projectName, + version: '1.0.0', + description: 'A test project for e2e testing', + }, + null, + 2 + ) + ); + + // Create a README.md + fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${projectName}\n\nA test project.`); + + // Create a src directory with an index.ts file + fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true }); + fs.writeFileSync( + path.join(projectPath, 'src', 'index.ts'), + 'export const hello = () => console.log("Hello World");' + ); + + // Set up welcome view with the project in recent projects (but NOT as current project) + await setupWelcomeView(page, { + recentProjects: [ + { + id: projectId, + name: projectName, + path: projectPath, + lastOpened: new Date(Date.now() - 86400000).toISOString(), // 1 day ago + }, + ], + }); + + // Navigate to the app + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Wait for welcome view to be visible + await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); + + // Verify we see the "Recent Projects" section + await expect(page.getByText('Recent Projects')).toBeVisible({ timeout: 5000 }); + + // Click on the recent project to open it + const recentProjectCard = page.locator(`[data-testid="recent-project-${projectId}"]`); + await expect(recentProjectCard).toBeVisible(); + await recentProjectCard.click(); + + // Wait for the board view to appear (project was opened) + await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); + + // Verify the project name appears in the project selector (sidebar) + await expect( + page.locator('[data-testid="project-selector"]').getByText(projectName) + ).toBeVisible({ timeout: 5000 }); + + // Verify .automaker directory was created (initialized for the first time) + // Use polling since file creation may be async + const automakerDir = path.join(projectPath, '.automaker'); + await expect(async () => { + expect(fs.existsSync(automakerDir)).toBe(true); + }).toPass({ timeout: 10000 }); + + // Verify the required structure was created by initializeProject: + // - .automaker/categories.json + // - .automaker/features directory + // - .automaker/context directory + // Note: app_spec.txt is NOT created automatically for existing projects + const categoriesPath = path.join(automakerDir, 'categories.json'); + await expect(async () => { + expect(fs.existsSync(categoriesPath)).toBe(true); + }).toPass({ timeout: 10000 }); + + // Verify subdirectories were created + expect(fs.existsSync(path.join(automakerDir, 'features'))).toBe(true); + expect(fs.existsSync(path.join(automakerDir, 'context'))).toBe(true); + + // Verify the original project files still exist (weren't modified) + expect(fs.existsSync(path.join(projectPath, 'package.json'))).toBe(true); + expect(fs.existsSync(path.join(projectPath, 'README.md'))).toBe(true); + expect(fs.existsSync(path.join(projectPath, 'src', 'index.ts'))).toBe(true); + }); +}); diff --git a/apps/ui/tests/project-creation.spec.ts b/apps/ui/tests/project-creation.spec.ts new file mode 100644 index 00000000..9d71d4ff --- /dev/null +++ b/apps/ui/tests/project-creation.spec.ts @@ -0,0 +1,188 @@ +/** + * Project Creation End-to-End Tests + * + * Tests the project creation flows: + * 1. Creating a new blank project from the welcome view + * 2. Creating a new project from a GitHub template + */ + +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; +import { createTempDirPath, cleanupTempDir, setupWelcomeView } from './utils'; + +// Create unique temp dir for this test run +const TEST_TEMP_DIR = createTempDirPath('project-creation-test'); + +test.describe('Project Creation', () => { + test.beforeAll(async () => { + // Create test temp directory + if (!fs.existsSync(TEST_TEMP_DIR)) { + fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); + } + }); + + test.afterAll(async () => { + // Cleanup temp directory + cleanupTempDir(TEST_TEMP_DIR); + }); + + test('should create a new blank project from welcome view', async ({ page }) => { + const projectName = `test-project-${Date.now()}`; + + // Set up welcome view with workspace directory pre-configured + await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR }); + + // Navigate to the app + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Wait for welcome view to be visible + await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); + + // Click the "Create New Project" dropdown button + const createButton = page.locator('[data-testid="create-new-project"]'); + await expect(createButton).toBeVisible(); + await createButton.click(); + + // Click "Quick Setup" option from the dropdown + const quickSetupOption = page.locator('[data-testid="quick-setup-option"]'); + await expect(quickSetupOption).toBeVisible(); + await quickSetupOption.click(); + + // Wait for the new project modal to appear + const modal = page.locator('[data-testid="new-project-modal"]'); + await expect(modal).toBeVisible({ timeout: 5000 }); + + // Enter the project name + const projectNameInput = page.locator('[data-testid="project-name-input"]'); + await expect(projectNameInput).toBeVisible(); + await projectNameInput.fill(projectName); + + // Verify the workspace directory is shown (from our pre-configured localStorage) + // Wait for workspace to be loaded (it shows "Will be created at:" when ready) + await expect(page.getByText('Will be created at:')).toBeVisible({ timeout: 5000 }); + + // Click the Create Project button + const createProjectButton = page.locator('[data-testid="confirm-create-project"]'); + await expect(createProjectButton).toBeVisible(); + await createProjectButton.click(); + + // Wait for project creation to complete + // The app may show an init dialog briefly and then navigate to board view + // We just need to verify we end up on the board view with our project + + // Wait for the board view - this confirms the project was created and opened + await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); + + // Verify the project name appears in the project selector (sidebar) + await expect( + page.locator('[data-testid="project-selector"]').getByText(projectName) + ).toBeVisible({ timeout: 5000 }); + + // Verify the project was created in the filesystem + const projectPath = path.join(TEST_TEMP_DIR, projectName); + expect(fs.existsSync(projectPath)).toBe(true); + + // Verify .automaker directory was created + const automakerDir = path.join(projectPath, '.automaker'); + expect(fs.existsSync(automakerDir)).toBe(true); + + // Verify app_spec.txt was created + const appSpecPath = path.join(automakerDir, 'app_spec.txt'); + expect(fs.existsSync(appSpecPath)).toBe(true); + + // Verify the app_spec.txt contains the project name + const appSpecContent = fs.readFileSync(appSpecPath, 'utf-8'); + expect(appSpecContent).toContain(projectName); + }); + + test('should create a new project from GitHub template', async ({ page }) => { + // Increase timeout for this test since git clone takes time + test.setTimeout(60000); + + const projectName = `template-project-${Date.now()}`; + + // Set up welcome view with workspace directory pre-configured + await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR }); + + // Navigate to the app + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Wait for welcome view to be visible + await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); + + // Click the "Create New Project" dropdown button + const createButton = page.locator('[data-testid="create-new-project"]'); + await expect(createButton).toBeVisible(); + await createButton.click(); + + // Click "Quick Setup" option from the dropdown + const quickSetupOption = page.locator('[data-testid="quick-setup-option"]'); + await expect(quickSetupOption).toBeVisible(); + await quickSetupOption.click(); + + // Wait for the new project modal to appear + const modal = page.locator('[data-testid="new-project-modal"]'); + await expect(modal).toBeVisible({ timeout: 5000 }); + + // Enter the project name first + const projectNameInput = page.locator('[data-testid="project-name-input"]'); + await expect(projectNameInput).toBeVisible(); + await projectNameInput.fill(projectName); + + // Wait for workspace directory to be loaded + await expect(page.getByText('Will be created at:')).toBeVisible({ timeout: 5000 }); + + // Click on the "Starter Kit" tab + const starterKitTab = modal.getByText('Starter Kit'); + await expect(starterKitTab).toBeVisible(); + await starterKitTab.click(); + + // Select the first template (Automaker Starter Kit) + const firstTemplate = page.locator('[data-testid="template-automaker-starter-kit"]'); + await expect(firstTemplate).toBeVisible(); + await firstTemplate.click(); + + // Verify the template is selected (check mark should appear) + await expect(firstTemplate.locator('.lucide-check')).toBeVisible(); + + // Click the Create Project button + const createProjectButton = page.locator('[data-testid="confirm-create-project"]'); + await expect(createProjectButton).toBeVisible(); + await createProjectButton.click(); + + // Wait for git clone to complete and board view to appear + // This takes longer due to the git clone operation + await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 45000 }); + + // Verify the project name appears in the project selector (sidebar) + await expect( + page.locator('[data-testid="project-selector"]').getByText(projectName) + ).toBeVisible({ timeout: 5000 }); + + // Verify the project was cloned in the filesystem + const projectPath = path.join(TEST_TEMP_DIR, projectName); + expect(fs.existsSync(projectPath)).toBe(true); + + // Verify .automaker directory was created + const automakerDir = path.join(projectPath, '.automaker'); + expect(fs.existsSync(automakerDir)).toBe(true); + + // Verify app_spec.txt was created with template info + const appSpecPath = path.join(automakerDir, 'app_spec.txt'); + expect(fs.existsSync(appSpecPath)).toBe(true); + const appSpecContent = fs.readFileSync(appSpecPath, 'utf-8'); + expect(appSpecContent).toContain(projectName); + expect(appSpecContent).toContain('Automaker Starter Kit'); + + // Verify the template files were cloned (check for package.json which should exist in the template) + const packageJsonPath = path.join(projectPath, 'package.json'); + expect(fs.existsSync(packageJsonPath)).toBe(true); + + // Verify it's a git repository (cloned from GitHub) + const gitDir = path.join(projectPath, '.git'); + expect(fs.existsSync(gitDir)).toBe(true); + }); +}); diff --git a/apps/ui/tests/utils/project/setup.ts b/apps/ui/tests/utils/project/setup.ts index ee24b376..23f87d8d 100644 --- a/apps/ui/tests/utils/project/setup.ts +++ b/apps/ui/tests/utils/project/setup.ts @@ -1,5 +1,164 @@ import { Page } from '@playwright/test'; +/** + * Store version constants - centralized to avoid hardcoding across tests + * These MUST match the versions used in the actual stores + */ +const STORE_VERSIONS = { + APP_STORE: 2, // Must match app-store.ts persist version + SETUP_STORE: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0 +} as const; + +/** + * Project interface for test setup + */ +export interface TestProject { + id: string; + name: string; + path: string; + lastOpened?: string; +} + +/** + * Options for setting up the welcome view + */ +export interface WelcomeViewSetupOptions { + /** Directory path to pre-configure as the workspace directory */ + workspaceDir?: string; + /** Recent projects to show (but not as current project) */ + recentProjects?: TestProject[]; +} + +/** + * Set up localStorage to show the welcome view with no current project + * This is the cleanest way to test project creation flows + * + * @param page - Playwright page + * @param options - Configuration options + */ +export async function setupWelcomeView( + page: Page, + options?: WelcomeViewSetupOptions +): Promise { + await page.addInitScript( + ({ + opts, + versions, + }: { + opts: WelcomeViewSetupOptions | undefined; + versions: typeof STORE_VERSIONS; + }) => { + // Set up empty app state (no current project) - shows welcome view + const appState = { + state: { + projects: opts?.recentProjects || [], + currentProject: null, + currentView: 'welcome', + theme: 'dark', + sidebarOpen: true, + apiKeys: { anthropic: '', google: '' }, + chatSessions: [], + chatHistoryOpen: false, + maxConcurrency: 3, + }, + version: versions.APP_STORE, + }; + localStorage.setItem('automaker-storage', JSON.stringify(appState)); + + // Mark setup as complete to skip the setup wizard + const setupState = { + state: { + isFirstRun: false, + setupComplete: true, + skipClaudeSetup: false, + }, + version: versions.SETUP_STORE, + }; + localStorage.setItem('automaker-setup', JSON.stringify(setupState)); + + // Set workspace directory if provided + if (opts?.workspaceDir) { + localStorage.setItem('automaker:lastProjectDir', opts.workspaceDir); + } + }, + { opts: options, versions: STORE_VERSIONS } + ); +} + +/** + * Set up localStorage with a project at a real filesystem path + * Use this when testing with actual files on disk + * + * @param page - Playwright page + * @param projectPath - Absolute path to the project directory + * @param projectName - Display name for the project + * @param options - Additional options + */ +export async function setupRealProject( + page: Page, + projectPath: string, + projectName: string, + options?: { + /** Set as current project (opens board view) or just add to recent projects */ + setAsCurrent?: boolean; + /** Additional recent projects to include */ + additionalProjects?: TestProject[]; + } +): Promise { + await page.addInitScript( + ({ + path, + name, + opts, + versions, + }: { + path: string; + name: string; + opts: typeof options; + versions: typeof STORE_VERSIONS; + }) => { + const projectId = `project-${Date.now()}`; + const project: TestProject = { + id: projectId, + name: name, + path: path, + lastOpened: new Date().toISOString(), + }; + + const allProjects = [project, ...(opts?.additionalProjects || [])]; + const currentProject = opts?.setAsCurrent !== false ? project : null; + + const appState = { + state: { + projects: allProjects, + currentProject: currentProject, + currentView: currentProject ? 'board' : 'welcome', + theme: 'dark', + sidebarOpen: true, + apiKeys: { anthropic: '', google: '' }, + chatSessions: [], + chatHistoryOpen: false, + maxConcurrency: 3, + }, + version: versions.APP_STORE, + }; + localStorage.setItem('automaker-storage', JSON.stringify(appState)); + + // Mark setup as complete + const setupState = { + state: { + isFirstRun: false, + setupComplete: true, + skipClaudeSetup: false, + }, + version: versions.SETUP_STORE, + }; + localStorage.setItem('automaker-setup', JSON.stringify(setupState)); + }, + { path: projectPath, name: projectName, opts: options, versions: STORE_VERSIONS } + ); +} + /** * Set up a mock project in localStorage to bypass the welcome screen * This simulates having opened a project before @@ -595,7 +754,7 @@ export async function setupFirstRun(page: Page): Promise { * Set up the app to skip the setup wizard (setup already complete) */ export async function setupComplete(page: Page): Promise { - await page.addInitScript(() => { + await page.addInitScript((versions: typeof STORE_VERSIONS) => { // Mark setup as complete const setupState = { state: { @@ -604,11 +763,11 @@ export async function setupComplete(page: Page): Promise { currentStep: 'complete', skipClaudeSetup: false, }, - version: 2, // Must match app-store.ts persist version + version: versions.SETUP_STORE, }; localStorage.setItem('automaker-setup', JSON.stringify(setupState)); - }); + }, STORE_VERSIONS); } /** From edef4c7cee3fc239798a44576441e9fed901e1ce Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 13:13:47 -0500 Subject: [PATCH 4/4] refactor: optimize issue and PR fetching by using parallel execution - Updated the list-issues and list-prs handlers to fetch open and closed issues, as well as open and merged PRs in parallel, improving performance. - Removed the redundant 'issues' and 'prs' properties from the result interfaces to streamline the response structure. - Added 'skipTests' flag in integration tests to indicate tests that should be skipped, enhancing test management. --- .../src/routes/github/routes/list-issues.ts | 37 ++++++++++--------- .../src/routes/github/routes/list-prs.ts | 37 +++++++++---------- .../auto-mode-service.integration.test.ts | 6 +++ 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/apps/server/src/routes/github/routes/list-issues.ts b/apps/server/src/routes/github/routes/list-issues.ts index 49850242..08f94135 100644 --- a/apps/server/src/routes/github/routes/list-issues.ts +++ b/apps/server/src/routes/github/routes/list-issues.ts @@ -28,7 +28,6 @@ export interface GitHubIssue { export interface ListIssuesResult { success: boolean; - issues?: GitHubIssue[]; openIssues?: GitHubIssue[]; closedIssues?: GitHubIssue[]; error?: string; @@ -54,23 +53,26 @@ export function createListIssuesHandler() { return; } - // Fetch open issues - const { stdout: openStdout } = await execAsync( - 'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100', - { - cwd: projectPath, - env: execEnv, - } - ); + // Fetch open and closed issues in parallel + const [openResult, closedResult] = await Promise.all([ + execAsync( + 'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ), + execAsync( + 'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ), + ]); - // Fetch closed issues - const { stdout: closedStdout } = await execAsync( - 'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50', - { - cwd: projectPath, - env: execEnv, - } - ); + const { stdout: openStdout } = openResult; + const { stdout: closedStdout } = closedResult; const openIssues: GitHubIssue[] = JSON.parse(openStdout || '[]'); const closedIssues: GitHubIssue[] = JSON.parse(closedStdout || '[]'); @@ -79,7 +81,6 @@ export function createListIssuesHandler() { success: true, openIssues, closedIssues, - issues: [...openIssues, ...closedIssues], }); } catch (error) { logError(error, 'List GitHub issues failed'); diff --git a/apps/server/src/routes/github/routes/list-prs.ts b/apps/server/src/routes/github/routes/list-prs.ts index 2dd0b2c4..87f42a38 100644 --- a/apps/server/src/routes/github/routes/list-prs.ts +++ b/apps/server/src/routes/github/routes/list-prs.ts @@ -32,7 +32,6 @@ export interface GitHubPR { export interface ListPRsResult { success: boolean; - prs?: GitHubPR[]; openPRs?: GitHubPR[]; mergedPRs?: GitHubPR[]; error?: string; @@ -58,23 +57,24 @@ export function createListPRsHandler() { return; } - // Fetch open PRs - const { stdout: openStdout } = await execAsync( - 'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100', - { - cwd: projectPath, - env: execEnv, - } - ); - - // Fetch merged PRs - const { stdout: mergedStdout } = await execAsync( - 'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50', - { - cwd: projectPath, - env: execEnv, - } - ); + const [openResult, mergedResult] = await Promise.all([ + execAsync( + 'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ), + execAsync( + 'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ), + ]); + const { stdout: openStdout } = openResult; + const { stdout: mergedStdout } = mergedResult; const openPRs: GitHubPR[] = JSON.parse(openStdout || '[]'); const mergedPRs: GitHubPR[] = JSON.parse(mergedStdout || '[]'); @@ -83,7 +83,6 @@ export function createListPRsHandler() { success: true, openPRs, mergedPRs, - prs: [...openPRs, ...mergedPRs], }); } catch (error) { logError(error, 'List GitHub PRs failed'); diff --git a/apps/server/tests/integration/services/auto-mode-service.integration.test.ts b/apps/server/tests/integration/services/auto-mode-service.integration.test.ts index d9d6ee13..e0ab4c4d 100644 --- a/apps/server/tests/integration/services/auto-mode-service.integration.test.ts +++ b/apps/server/tests/integration/services/auto-mode-service.integration.test.ts @@ -146,6 +146,7 @@ describe('auto-mode-service.ts (integration)', () => { category: 'test', description: 'Test without worktree', status: 'pending', + skipTests: true, }); const mockProvider = { @@ -181,6 +182,7 @@ describe('auto-mode-service.ts (integration)', () => { category: 'ui', description: 'Execute this feature', status: 'pending', + skipTests: true, }); const mockProvider = { @@ -327,6 +329,7 @@ describe('auto-mode-service.ts (integration)', () => { category: 'test', description: 'Auto feature 1', status: 'pending', + skipTests: true, }); await createTestFeature(testRepo.path, 'auto-2', { @@ -334,6 +337,7 @@ describe('auto-mode-service.ts (integration)', () => { category: 'test', description: 'Auto feature 2', status: 'pending', + skipTests: true, }); const mockProvider = { @@ -520,6 +524,7 @@ describe('auto-mode-service.ts (integration)', () => { description: 'Feature with skip planning', status: 'pending', planningMode: 'skip', + skipTests: true, }); const mockProvider = { @@ -555,6 +560,7 @@ describe('auto-mode-service.ts (integration)', () => { status: 'pending', planningMode: 'lite', requirePlanApproval: false, + skipTests: true, }); const mockProvider = {