Files
Aperant/apps/frontend/src/renderer/lib/browser-mock.ts
T
Andy d081af0422 auto-claude: 151-fix-pr-review-agent-token-refresh-on-account-swap (#1456)
* auto-claude: subtask-1-1 - Add GITHUB_AUTH_CHANGED IPC channel constant

* auto-claude: subtask-1-2 - Add async getGitHubTokenForSubprocess() helper to utils.ts

Add a new exported async function getGitHubTokenForSubprocess() that calls
getTokenFromGhCli() to retrieve fresh GitHub tokens for subprocess use.
This provides a clean interface for runner-env.ts to get tokens without
caching, ensuring account changes are reflected immediately.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-1-3 - Update getRunnerEnv() to include GITHUB_TOKEN

* auto-claude: subtask-2-1 - Add auth change detection and event emission to oauth-handlers.ts

- Add GitHubAuthChangedPayload interface for auth change events
- Add sendAuthChangedToRenderer() to broadcast auth changes to all windows
- Add getCurrentGitHubUsername() helper to get current GitHub user
- Modify registerStartGhAuth() to:
  - Capture username before auth starts
  - Get username after successful auth
  - Emit GITHUB_AUTH_CHANGED event if account changed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-1 - Add onGitHubAuthChanged listener to GitHubAPI interface

- Add onGitHubAuthChanged to GitHubAPI interface in github-api.ts
- Add implementation using createIpcListener with IPC_CHANNELS.GITHUB_AUTH_CHANGED
- Add mock implementation in browser-mock.ts for testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-4-1 - Add GitHub auth change listener to pr-review-store

* fix: Address PR review findings for GitHub auth handlers

- Convert getCurrentGitHubUsername() to async using promisified execFile
  to avoid blocking Electron main thread during auth flow (finding #1)
- Make getGitHubTokenForSubprocess() truly async by introducing async
  getTokenFromGhCliAsync() - the sync version is preserved for
  getGitHubConfig (finding #2)
- Add warning log when username fetch fails after successful auth,
  handling the edge case where auth succeeds but account change
  detection fails (finding #3)
- Remove unused timestamp field from GitHubAuthChangedPayload interface
  since the renderer callback only uses oldUsername/newUsername (finding #4)
- Document the intentional extraEnv override behavior in getRunnerEnv()
  JSDoc comment (finding #5)

All 5 findings from PR review were real issues. These fixes improve code
quality by avoiding main thread blocking and clarifying edge cases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: Fix oauth-handlers tests for async getCurrentGitHubUsername

Update test mocks and add waitForAsyncSetup helper to handle the async
changes in getCurrentGitHubUsername(). The function now uses promisified
execFile instead of execFileSync to avoid blocking the main thread.

Key changes:
- Add mockExecFile mock for the promisified execFile function
- Add waitForAsyncSetup helper to wait for async setup before emitting
  process events
- Update all affected tests to use waitForAsyncSetup before emitting
  mock process events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:30:51 +01:00

366 lines
11 KiB
TypeScript

/**
* Browser mock for window.electronAPI
* This allows the app to run in a regular browser for UI development/testing
*
* This module aggregates all mock implementations from separate modules
* for better code organization and maintainability.
*/
import type { ElectronAPI } from '../../shared/types';
import {
projectMock,
taskMock,
workspaceMock,
terminalMock,
claudeProfileMock,
contextMock,
integrationMock,
changelogMock,
insightsMock,
infrastructureMock,
settingsMock
} from './mocks';
// Check if we're in a browser (not Electron)
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined;
/**
* Create mock electronAPI for browser
* Aggregates all mock implementations from separate modules
*/
const browserMockAPI: ElectronAPI = {
// Project Operations
...projectMock,
// Task Operations
...taskMock,
// Workspace Management
...workspaceMock,
// Terminal Operations
...terminalMock,
// Claude Profile Management
...claudeProfileMock,
// Settings
...settingsMock,
// Roadmap Operations
getRoadmap: async () => ({
success: true,
data: null
}),
getRoadmapStatus: async () => ({
success: true,
data: { isRunning: false }
}),
saveRoadmap: async () => ({
success: true
}),
generateRoadmap: (_projectId: string, _enableCompetitorAnalysis?: boolean, _refreshCompetitorAnalysis?: boolean) => {
console.warn('[Browser Mock] generateRoadmap called');
},
refreshRoadmap: (_projectId: string, _enableCompetitorAnalysis?: boolean, _refreshCompetitorAnalysis?: boolean) => {
console.warn('[Browser Mock] refreshRoadmap called');
},
updateFeatureStatus: async () => ({ success: true }),
convertFeatureToSpec: async (projectId: string, _featureId: string) => ({
success: true,
data: {
id: `task-${Date.now()}`,
specId: '',
projectId,
title: 'Converted Feature',
description: 'Feature converted from roadmap',
status: 'backlog' as const,
subtasks: [],
logs: [],
createdAt: new Date(),
updatedAt: new Date()
}
}),
stopRoadmap: async () => ({ success: true }),
// Roadmap Progress Persistence
saveRoadmapProgress: async () => ({ success: true }),
loadRoadmapProgress: async () => ({ success: true, data: null }),
clearRoadmapProgress: async () => ({ success: true }),
// Roadmap Event Listeners
onRoadmapProgress: () => () => {},
onRoadmapComplete: () => () => {},
onRoadmapError: () => () => {},
onRoadmapStopped: () => () => {},
// Context Operations
...contextMock,
// Environment Configuration & Integration Operations
...integrationMock,
// Changelog & Release Operations
...changelogMock,
// Insights Operations
...insightsMock,
// Infrastructure & Docker Operations
...infrastructureMock,
// API Profile Management (custom Anthropic-compatible endpoints)
getAPIProfiles: async () => ({
success: true,
data: {
profiles: [],
activeProfileId: null,
version: 1
}
}),
saveAPIProfile: async (profile) => ({
success: true,
data: {
id: `mock-profile-${Date.now()}`,
...profile,
createdAt: Date.now(),
updatedAt: Date.now()
}
}),
updateAPIProfile: async (profile) => ({
success: true,
data: {
...profile,
updatedAt: Date.now()
}
}),
deleteAPIProfile: async (_profileId: string) => ({
success: true
}),
setActiveAPIProfile: async (_profileId: string | null) => ({
success: true
}),
testConnection: async (_baseUrl: string, _apiKey: string, _signal?: AbortSignal) => ({
success: true,
data: {
success: true,
message: 'Connection successful (mock)'
}
}),
discoverModels: async (_baseUrl: string, _apiKey: string, _signal?: AbortSignal) => ({
success: true,
data: {
models: []
}
}),
// GitHub API
github: {
getGitHubRepositories: async () => ({ success: true, data: [] }),
getGitHubIssues: async () => ({ success: true, data: { issues: [], hasMore: false } }),
getGitHubIssue: async () => ({ success: true, data: null as any }),
getIssueComments: async () => ({ success: true, data: [] }),
checkGitHubConnection: async () => ({ success: true, data: { connected: false, repoFullName: undefined, error: undefined } }),
investigateGitHubIssue: () => {},
importGitHubIssues: async () => ({ success: true, data: { success: true, imported: 0, failed: 0, issues: [] } }),
createGitHubRelease: async () => ({ success: true, data: { url: '' } }),
suggestReleaseVersion: async () => ({ success: true, data: { suggestedVersion: '1.0.0', currentVersion: '0.0.0', bumpType: 'minor' as const, commitCount: 0, reason: 'Initial' } }),
checkGitHubCli: async () => ({ success: true, data: { installed: false } }),
checkGitHubAuth: async () => ({ success: true, data: { authenticated: false } }),
startGitHubAuth: async () => ({ success: true, data: { success: false } }),
getGitHubToken: async () => ({ success: true, data: { token: '' } }),
getGitHubUser: async () => ({ success: true, data: { username: '' } }),
listGitHubUserRepos: async () => ({ success: true, data: { repos: [] } }),
detectGitHubRepo: async () => ({ success: true, data: '' }),
getGitHubBranches: async () => ({ success: true, data: [] }),
createGitHubRepo: async () => ({ success: true, data: { fullName: '', url: '' } }),
addGitRemote: async () => ({ success: true, data: { remoteUrl: '' } }),
listGitHubOrgs: async () => ({ success: true, data: { orgs: [] } }),
onGitHubAuthDeviceCode: () => () => {},
onGitHubAuthChanged: () => () => {},
onGitHubInvestigationProgress: () => () => {},
onGitHubInvestigationComplete: () => () => {},
onGitHubInvestigationError: () => () => {},
getAutoFixConfig: async () => null,
saveAutoFixConfig: async () => true,
getAutoFixQueue: async () => [],
checkAutoFixLabels: async () => [],
checkNewIssues: async () => [],
startAutoFix: () => {},
onAutoFixProgress: () => () => {},
onAutoFixComplete: () => () => {},
onAutoFixError: () => () => {},
listPRs: async () => [],
getPR: async () => null,
runPRReview: () => {},
cancelPRReview: async () => true,
postPRReview: async () => true,
postPRComment: async () => true,
mergePR: async () => true,
assignPR: async () => true,
markReviewPosted: async () => true,
getPRReview: async () => null,
getPRReviewsBatch: async () => ({}),
deletePRReview: async () => true,
checkNewCommits: async () => ({ hasNewCommits: false, newCommitCount: 0 }),
checkMergeReadiness: async () => ({ isDraft: false, mergeable: 'UNKNOWN' as const, isBehind: false, ciStatus: 'none' as const, blockers: [] }),
updatePRBranch: async () => ({ success: true }),
runFollowupReview: () => {},
getPRLogs: async () => null,
getWorkflowsAwaitingApproval: async () => ({ awaiting_approval: 0, workflow_runs: [], can_approve: false }),
approveWorkflow: async () => true,
onPRReviewProgress: () => () => {},
onPRReviewComplete: () => () => {},
onPRReviewError: () => () => {},
batchAutoFix: () => {},
getBatches: async () => [],
onBatchProgress: () => () => {},
onBatchComplete: () => () => {},
onBatchError: () => () => {},
// Analyze & Group Issues (proactive workflow)
analyzeIssuesPreview: () => {},
approveBatches: async () => ({ success: true, batches: [] }),
onAnalyzePreviewProgress: () => () => {},
onAnalyzePreviewComplete: () => () => {},
onAnalyzePreviewError: () => () => {}
},
// Claude Code Operations
checkClaudeCodeVersion: async () => ({
success: true,
data: {
installed: '1.0.0',
latest: '1.0.0',
isOutdated: false,
path: '/usr/local/bin/claude',
detectionResult: {
found: true,
version: '1.0.0',
path: '/usr/local/bin/claude',
source: 'system-path' as const,
message: 'Claude Code CLI found'
}
}
}),
installClaudeCode: async () => ({
success: true,
data: { command: 'npm install -g @anthropic-ai/claude-code' }
}),
getClaudeCodeVersions: async () => ({
success: true,
data: {
versions: ['1.0.5', '1.0.4', '1.0.3', '1.0.2', '1.0.1', '1.0.0']
}
}),
installClaudeCodeVersion: async (version: string) => ({
success: true,
data: { command: `npm install -g @anthropic-ai/claude-code@${version}`, version }
}),
getClaudeCodeInstallations: async () => ({
success: true,
data: {
installations: [
{
path: '/usr/local/bin/claude',
version: '1.0.0',
source: 'system-path' as const,
isActive: true,
}
],
activePath: '/usr/local/bin/claude',
}
}),
setClaudeCodeActivePath: async (cliPath: string) => ({
success: true,
data: { path: cliPath }
}),
// Terminal Worktree Operations
createTerminalWorktree: async () => ({
success: false,
error: 'Not available in browser mode'
}),
listTerminalWorktrees: async () => ({
success: true,
data: []
}),
removeTerminalWorktree: async () => ({
success: false,
error: 'Not available in browser mode'
}),
listOtherWorktrees: async () => ({
success: true,
data: []
}),
// MCP Server Health Check Operations
checkMcpHealth: async (server) => ({
success: true,
data: {
serverId: server.id,
status: 'unknown' as const,
message: 'Health check not available in browser mode',
checkedAt: new Date().toISOString()
}
}),
testMcpConnection: async (server) => ({
success: true,
data: {
serverId: server.id,
success: false,
message: 'Connection test not available in browser mode'
}
}),
// Screenshot capture operations
getSources: async () => ({
success: true,
data: []
}),
capture: async (_options: { sourceId: string }) => ({
success: false,
error: 'Screenshot capture not available in browser mode'
}),
// Debug Operations
getDebugInfo: async () => ({
systemInfo: {
appVersion: '0.0.0-browser-mock',
platform: 'browser',
isPackaged: 'false'
},
recentErrors: [],
logsPath: '/mock/logs',
debugReport: '[Browser Mock] Debug report not available in browser mode'
}),
openLogsFolder: async () => ({ success: false, error: 'Not available in browser mode' }),
copyDebugInfo: async () => ({ success: false, error: 'Not available in browser mode' }),
getRecentErrors: async () => [],
listLogFiles: async () => []
};
/**
* Initialize browser mock if not running in Electron
*/
export function initBrowserMock(): void {
if (!isElectron) {
console.warn('%c[Browser Mock] Initializing mock electronAPI for browser preview', 'color: #f0ad4e; font-weight: bold;');
(window as Window & { electronAPI: ElectronAPI }).electronAPI = browserMockAPI;
}
}
// Auto-initialize
initBrowserMock();