auto-claude: 147-remove-outdated-compatibility-shims (#1465)
* auto-claude: subtask-1-1 - Remove validation_strategy backward compatibility shim - Delete apps/backend/validation_strategy.py shim file that re-exported from spec.validation_strategy - Update docstring in spec/validation_strategy.py to show correct import path Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-2 - Remove service_orchestrator shim - Deleted apps/backend/service_orchestrator.py backward compatibility shim file - Updated docstring in services/orchestrator.py to use correct import path (from services.orchestrator import instead of from service_orchestrator import) - Verified no external imports of the shim remain in the codebase - Import from services.orchestrator works correctly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-2-1 - Remove Chunk/ChunkStatus aliases from implementation_plan Removed backwards compatibility aliases as part of cleaning up outdated compatibility shims: - Removed ChunkStatus = SubtaskStatus from enums.py - Removed Chunk = Subtask from subtask.py - Removed Chunk/ChunkStatus exports from __init__.py - Updated all test files to use canonical names (Subtask, SubtaskStatus) This completes subtasks 2-1, 2-2, and 2-3 together since the test files depend on all three changes being made atomically. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-2-4 - Remove deprecated use_orchestrator_review field from GitHub runner models * auto-claude: subtask-3-1 - Update apps/frontend/src/main/index.ts to use platform imports * auto-claude: subtask-3-2 - Update python-detector.ts to use platform imports - Import isWindows from ./platform module - Replace all process.platform === 'win32' checks with isWindows() calls - Remove redundant local isWindows variable declarations * auto-claude: subtask-3-3 - Update apps/frontend/src/main/claude-cli-utils.ts to use platform imports * auto-claude: subtask-3-4 - Update apps/frontend/src/main/config-paths.ts to use platform imports * auto-claude: subtask-3-5 - Update apps/frontend/src/main/memory-service.ts to use platform imports * auto-claude: subtask-4-1 - Update claude-code-handlers.ts to use platform imports Replace all direct process.platform checks with centralized platform abstraction functions from ../platform module: - isWindows() for Windows platform checks - isMacOS() for macOS/Darwin platform checks - isLinux() for Linux platform checks This removes 6 instances of process.platform === '...' comparisons and 1 local isWindows variable assignment, replacing them with the platform abstraction layer for better cross-platform consistency. * auto-claude: subtask-4-2 - Update apps/frontend/src/main/ipc-handlers/mcp-handlers.ts to use platform imports * auto-claude: subtask-4-3 - Update apps/frontend/src/main/ipc-handlers/memory-handlers.ts to use platform imports - Replace process.platform checks with platform module functions - Add getOllamaExecutablePaths(), getOllamaInstallCommand(), and getWhichCommand() to platform/paths.ts - Export new functions from platform/index.ts - Migrate checkOllamaInstalled() to use platform module for path resolution - Migrate getOllamaInstallCommand() to delegate to platform module - Update debug log to use getCurrentOS() instead of process.platform Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-4-4 - Update apps/frontend/src/main/ipc-handlers/termina * auto-claude: subtask-4-5 - Update apps/frontend/src/main/ipc-handlers/github/ - Replace direct process.platform check with getWhichCommand() from platform abstraction - Import getWhichCommand from ../../platform for cross-platform which/where command * auto-claude: subtask-4-6 - Update apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts to use platform imports * auto-claude: subtask-5-1 - Update apps/frontend/src/main/agent/agent-process.ts Replace direct process.platform checks with platform abstraction: - Import isWindows from ../platform module - Replace `process.platform !== 'win32'` with `!isWindows()` - Replace `process.platform === 'win32'` with `isWindows()` This ensures consistent platform detection using the centralized platform abstraction layer. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-5-2 - Update apps/frontend/src/main/agent/agent-queue.ts * auto-claude: subtask-5-3 - Update apps/frontend/src/main/terminal/pty-daemon.ts to use platform imports * auto-claude: subtask-5-4 - Update pty-daemon-client.ts to use platform imports Replace direct process.platform === 'win32' check with isWindows() from the platform abstraction layer for consistent cross-platform handling of socket paths. * auto-claude: subtask-5-5 - Update apps/frontend/src/main/insights/config.ts to use platform imports - Import isWindows() from '../platform' - Replace process.platform === 'win32' checks with isWindows() - Maintains case-insensitive path comparison on Windows * auto-claude: subtask-5-6 - Update apps/frontend/src/main/changelog/version-suggester.ts to use platform imports * auto-claude: subtask-5-7 - Update apps/frontend/src/main/changelog/generator. * fix: Remove unused import and fix test import paths - Remove unused `isWindows` import from memory-handlers.ts - Fix test_service_orchestrator.py to import from services.orchestrator instead of the removed service_orchestrator shim - Fix case sensitivity in path ("Apps" -> "apps") Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Fix test import paths for case sensitivity and removed shims - Fix path case sensitivity: "Apps" -> "apps" in 21 test files - Update test_validation_strategy.py to import from spec.validation_strategy Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,8 @@ Package Structure:
|
||||
- factories.py: Factory functions for creating different plan types
|
||||
"""
|
||||
|
||||
# Export all public types and functions for backwards compatibility
|
||||
# Export all public types and functions
|
||||
from .enums import (
|
||||
ChunkStatus, # Backwards compatibility
|
||||
PhaseType,
|
||||
SubtaskStatus,
|
||||
VerificationType,
|
||||
@@ -40,7 +39,7 @@ from .factories import (
|
||||
)
|
||||
from .phase import Phase
|
||||
from .plan import ImplementationPlan
|
||||
from .subtask import Chunk, Subtask # Chunk is backwards compatibility alias
|
||||
from .subtask import Subtask
|
||||
from .verification import Verification
|
||||
|
||||
__all__ = [
|
||||
@@ -58,7 +57,4 @@ __all__ = [
|
||||
"create_feature_plan",
|
||||
"create_investigation_plan",
|
||||
"create_refactor_plan",
|
||||
# Backwards compatibility
|
||||
"Chunk",
|
||||
"ChunkStatus",
|
||||
]
|
||||
|
||||
@@ -51,7 +51,3 @@ class VerificationType(str, Enum):
|
||||
COMPONENT = "component" # Component renders correctly
|
||||
MANUAL = "manual" # Requires human verification
|
||||
NONE = "none" # No verification needed (investigation)
|
||||
|
||||
|
||||
# Backwards compatibility aliases
|
||||
ChunkStatus = SubtaskStatus
|
||||
|
||||
@@ -126,7 +126,3 @@ class Subtask:
|
||||
self.completed_at = None # Clear to maintain consistency (failed != completed)
|
||||
if reason:
|
||||
self.actual_output = f"FAILED: {reason}"
|
||||
|
||||
|
||||
# Backwards compatibility alias
|
||||
Chunk = Subtask
|
||||
|
||||
@@ -848,9 +848,6 @@ class GitHubRunnerConfig:
|
||||
auto_post_reviews: bool = False
|
||||
allow_fix_commits: bool = True
|
||||
review_own_prs: bool = False # Whether bot can review its own PRs
|
||||
use_orchestrator_review: bool = (
|
||||
True # DEPRECATED: No longer used, kept for config compatibility
|
||||
)
|
||||
use_parallel_orchestrator: bool = (
|
||||
True # Use SDK subagent parallel orchestrator (default)
|
||||
)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Backward compatibility shim - import from services.orchestrator instead."""
|
||||
|
||||
from services.orchestrator import (
|
||||
OrchestrationResult,
|
||||
ServiceConfig,
|
||||
ServiceContext,
|
||||
ServiceOrchestrator,
|
||||
get_service_config,
|
||||
is_multi_service_project,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ServiceConfig",
|
||||
"OrchestrationResult",
|
||||
"ServiceOrchestrator",
|
||||
"ServiceContext",
|
||||
"is_multi_service_project",
|
||||
"get_service_config",
|
||||
]
|
||||
@@ -11,7 +11,7 @@ The service orchestrator is used by:
|
||||
- Validation Strategy: To determine if multi-service orchestration is needed
|
||||
|
||||
Usage:
|
||||
from service_orchestrator import ServiceOrchestrator
|
||||
from services.orchestrator import ServiceOrchestrator
|
||||
|
||||
orchestrator = ServiceOrchestrator(project_dir)
|
||||
if orchestrator.is_multi_service():
|
||||
|
||||
@@ -11,7 +11,7 @@ The validation strategy is used by:
|
||||
- QA Agent: To determine what tests to create and run
|
||||
|
||||
Usage:
|
||||
from validation_strategy import ValidationStrategyBuilder
|
||||
from spec.validation_strategy import ValidationStrategyBuilder
|
||||
|
||||
builder = ValidationStrategyBuilder()
|
||||
strategy = builder.build_strategy(project_dir, spec_dir, "medium")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Backward compatibility shim - import from spec.validation_strategy instead."""
|
||||
|
||||
from spec.validation_strategy import * # noqa: F403
|
||||
@@ -24,7 +24,7 @@ import type { AppSettings } from '../../shared/types/settings';
|
||||
import { getOAuthModeClearVars } from './env-utils';
|
||||
import { getAugmentedEnv } from '../env-utils';
|
||||
import { getToolInfo, getClaudeCliPathForSdk } from '../cli-tool-manager';
|
||||
import { killProcessGracefully } from '../platform';
|
||||
import { killProcessGracefully, isWindows } from '../platform';
|
||||
|
||||
/**
|
||||
* Type for supported CLI tools
|
||||
@@ -42,7 +42,7 @@ const CLI_TOOL_ENV_MAP: Readonly<Record<CliTool, string>> = {
|
||||
|
||||
|
||||
function deriveGitBashPath(gitExePath: string): string | null {
|
||||
if (process.platform !== 'win32') {
|
||||
if (!isWindows()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export class AgentProcessManager {
|
||||
// On Windows, detect and pass git-bash path for Claude Code CLI
|
||||
// Electron can detect git via where.exe, but Python subprocess may not have the same PATH
|
||||
const gitBashEnv: Record<string, string> = {};
|
||||
if (process.platform === 'win32' && !process.env.CLAUDE_CODE_GIT_BASH_PATH) {
|
||||
if (isWindows() && !process.env.CLAUDE_CODE_GIT_BASH_PATH) {
|
||||
try {
|
||||
const gitInfo = getToolInfo('git');
|
||||
if (gitInfo.found && gitInfo.path) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { pythonEnvManager } from '../python-env-manager';
|
||||
import { transformIdeaFromSnakeCase, transformSessionFromSnakeCase } from '../ipc-handlers/ideation/transformers';
|
||||
import { transformRoadmapFromSnakeCase } from '../ipc-handlers/roadmap/transformers';
|
||||
import type { RawIdea } from '../ipc-handlers/ideation/types';
|
||||
import { getPathDelimiter } from '../platform';
|
||||
|
||||
/** Maximum length for status messages displayed in progress UI */
|
||||
const STATUS_MESSAGE_MAX_LENGTH = 200;
|
||||
@@ -348,7 +349,7 @@ export class AgentQueueManager {
|
||||
if (autoBuildSource) {
|
||||
pythonPathParts.push(autoBuildSource);
|
||||
}
|
||||
const combinedPythonPath = pythonPathParts.join(process.platform === 'win32' ? ';' : ':');
|
||||
const combinedPythonPath = pythonPathParts.join(getPathDelimiter());
|
||||
|
||||
// Build final environment with proper precedence:
|
||||
// 1. process.env (system)
|
||||
@@ -675,7 +676,7 @@ export class AgentQueueManager {
|
||||
if (autoBuildSource) {
|
||||
pythonPathParts.push(autoBuildSource);
|
||||
}
|
||||
const combinedPythonPath = pythonPathParts.join(process.platform === 'win32' ? ';' : ':');
|
||||
const combinedPythonPath = pythonPathParts.join(getPathDelimiter());
|
||||
|
||||
// Build final environment with proper precedence:
|
||||
// 1. process.env (system)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getCommits, getBranchDiffCommits } from './git-integration';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector';
|
||||
import { parsePythonCommand } from '../python-detector';
|
||||
import { getAugmentedEnv } from '../env-utils';
|
||||
import { isWindows } from '../platform';
|
||||
|
||||
/**
|
||||
* Core changelog generation logic
|
||||
@@ -245,7 +246,6 @@ export class ChangelogGenerator extends EventEmitter {
|
||||
*/
|
||||
private buildSpawnEnvironment(): Record<string, string> {
|
||||
const homeDir = os.homedir();
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Use getAugmentedEnv() to ensure common tool paths are available
|
||||
// even when app is launched from Finder/Dock
|
||||
@@ -265,7 +265,7 @@ export class ChangelogGenerator extends EventEmitter {
|
||||
...profileEnv, // Include active Claude profile config
|
||||
// Ensure critical env vars are set for claude CLI
|
||||
// Use USERPROFILE on Windows, HOME on Unix
|
||||
...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }),
|
||||
...(isWindows() ? { USERPROFILE: homeDir } : { HOME: homeDir }),
|
||||
USER: process.env.USER || process.env.USERNAME || 'user',
|
||||
PYTHONUNBUFFERED: '1',
|
||||
PYTHONIOENCODING: 'utf-8',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GitCommit } from '../../shared/types';
|
||||
import { getProfileEnv } from '../rate-limit-detector';
|
||||
import { parsePythonCommand } from '../python-detector';
|
||||
import { getAugmentedEnv } from '../env-utils';
|
||||
import { isWindows, requiresShell } from '../platform';
|
||||
|
||||
interface VersionSuggestion {
|
||||
version: string;
|
||||
@@ -144,7 +145,7 @@ Respond with ONLY a JSON object in this exact format (no markdown, no extra text
|
||||
|
||||
// Detect if this is a Windows batch file (.cmd or .bat)
|
||||
// These require shell=True in subprocess.run() because they need cmd.exe to execute
|
||||
const isCmdFile = /\.(cmd|bat)$/i.test(this.claudePath);
|
||||
const needsShell = requiresShell(this.claudePath);
|
||||
|
||||
return `
|
||||
import subprocess
|
||||
@@ -154,13 +155,13 @@ import sys
|
||||
prompt = "${escapedPrompt}"
|
||||
|
||||
try:
|
||||
# shell=${isCmdFile ? 'True' : 'False'} - Windows .cmd files require shell execution
|
||||
# shell=${needsShell ? 'True' : 'False'} - Windows .cmd files require shell execution
|
||||
result = subprocess.run(
|
||||
["${escapedClaudePath}", "chat", "--model", "haiku", "--prompt", prompt],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
shell=${isCmdFile ? 'True' : 'False'}
|
||||
shell=${needsShell ? 'True' : 'False'}
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
@@ -227,7 +228,6 @@ except Exception as e:
|
||||
*/
|
||||
private buildSpawnEnvironment(): Record<string, string> {
|
||||
const homeDir = os.homedir();
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Use getAugmentedEnv() to ensure common tool paths are available
|
||||
// even when app is launched from Finder/Dock
|
||||
@@ -240,7 +240,7 @@ except Exception as e:
|
||||
...augmentedEnv,
|
||||
...profileEnv,
|
||||
// Ensure critical env vars are set for claude CLI
|
||||
...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }),
|
||||
...(isWindows() ? { USERPROFILE: homeDir } : { HOME: homeDir }),
|
||||
USER: process.env.USER || process.env.USERNAME || 'user',
|
||||
PYTHONUNBUFFERED: '1',
|
||||
PYTHONIOENCODING: 'utf-8',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
import { getAugmentedEnv, getAugmentedEnvAsync } from './env-utils';
|
||||
import { getToolPath, getToolPathAsync } from './cli-tool-manager';
|
||||
import { isWindows, getPathDelimiter } from './platform';
|
||||
|
||||
export type ClaudeCliInvocation = {
|
||||
command: string;
|
||||
@@ -12,12 +13,12 @@ function ensureCommandDirInPath(command: string, env: Record<string, string>): R
|
||||
return env;
|
||||
}
|
||||
|
||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||
const pathSeparator = getPathDelimiter();
|
||||
const commandDir = path.dirname(command);
|
||||
const currentPath = env.PATH || '';
|
||||
const pathEntries = currentPath.split(pathSeparator);
|
||||
const normalizedCommandDir = path.normalize(commandDir);
|
||||
const hasCommandDir = process.platform === 'win32'
|
||||
const hasCommandDir = isWindows()
|
||||
? pathEntries
|
||||
.map((entry) => path.normalize(entry).toLowerCase())
|
||||
.includes(normalizedCommandDir.toLowerCase())
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { isLinux } from './platform';
|
||||
|
||||
const APP_NAME = 'auto-claude';
|
||||
|
||||
@@ -76,7 +77,7 @@ export function getMemoriesDir(): string {
|
||||
const legacyPath = path.join(os.homedir(), '.auto-claude', 'memories');
|
||||
|
||||
// On Linux with XDG variables set (AppImage, Flatpak, Snap), use XDG path
|
||||
if (process.platform === 'linux' && (process.env.XDG_DATA_HOME || process.env.APPIMAGE || process.env.SNAP || process.env.FLATPAK_ID)) {
|
||||
if (isLinux() && (process.env.XDG_DATA_HOME || process.env.APPIMAGE || process.env.SNAP || process.env.FLATPAK_ID)) {
|
||||
return path.join(getXdgDataHome(), APP_NAME, 'memories');
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ import { setupErrorLogging } from './app-logger';
|
||||
import { initSentryMain } from './sentry';
|
||||
import { preWarmToolCache } from './cli-tool-manager';
|
||||
import { initializeClaudeProfileManager } from './claude-profile-manager';
|
||||
import { isMacOS, isWindows } from './platform';
|
||||
import type { AppSettings } from '../shared/types';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -121,10 +122,10 @@ function getIconPath(): string {
|
||||
: join(process.resourcesPath);
|
||||
|
||||
let iconName: string;
|
||||
if (process.platform === 'darwin') {
|
||||
if (isMacOS()) {
|
||||
// Use PNG in dev mode (works better), ICNS in production
|
||||
iconName = is.dev ? 'icon-256.png' : 'icon.icns';
|
||||
} else if (process.platform === 'win32') {
|
||||
} else if (isWindows()) {
|
||||
iconName = 'icon.ico';
|
||||
} else {
|
||||
iconName = 'icon.png';
|
||||
@@ -245,13 +246,13 @@ function createWindow(): void {
|
||||
|
||||
// Set app name before ready (for dock tooltip on macOS in dev mode)
|
||||
app.setName('Auto Claude');
|
||||
if (process.platform === 'darwin') {
|
||||
if (isMacOS()) {
|
||||
// Force the name to appear in dock on macOS
|
||||
app.name = 'Auto Claude';
|
||||
}
|
||||
|
||||
// Fix Windows GPU cache permission errors (0x5 Access Denied)
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
app.commandLine.appendSwitch('disable-gpu-shader-disk-cache');
|
||||
app.commandLine.appendSwitch('disable-gpu-program-cache');
|
||||
console.log('[main] Applied Windows GPU cache fixes');
|
||||
@@ -263,7 +264,7 @@ app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId('com.autoclaude.ui');
|
||||
|
||||
// Clear cache on Windows to prevent permission errors from stale cache
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
session.defaultSession.clearCache()
|
||||
.then(() => console.log('[main] Cleared cache on startup'))
|
||||
.catch((err) => console.warn('[main] Failed to clear cache:', err));
|
||||
@@ -274,7 +275,7 @@ app.whenReady().then(() => {
|
||||
cleanupStaleUpdateMetadata();
|
||||
|
||||
// Set dock icon on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
if (isMacOS()) {
|
||||
const iconPath = getIconPath();
|
||||
try {
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
@@ -458,7 +459,7 @@ app.whenReady().then(() => {
|
||||
|
||||
// Quit when all windows are closed (except on macOS)
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
if (!isMacOS()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pythonEnvManager, getConfiguredPythonPath } from '../python-env-manager
|
||||
import { getValidatedPythonPath } from '../python-detector';
|
||||
import { getAugmentedEnv } from '../env-utils';
|
||||
import { getEffectiveSourcePath } from '../updater/path-resolver';
|
||||
import { isWindows } from '../platform';
|
||||
|
||||
/**
|
||||
* Configuration manager for insights service
|
||||
@@ -121,11 +122,11 @@ export class InsightsConfig {
|
||||
|
||||
if (autoBuildSource) {
|
||||
const normalizedAutoBuildSource = path.resolve(autoBuildSource);
|
||||
const autoBuildComparator = process.platform === 'win32'
|
||||
const autoBuildComparator = isWindows()
|
||||
? normalizedAutoBuildSource.toLowerCase()
|
||||
: normalizedAutoBuildSource;
|
||||
const hasAutoBuildSource = pythonPathParts.some((entry) => {
|
||||
const candidate = process.platform === 'win32' ? entry.toLowerCase() : entry;
|
||||
const candidate = isWindows() ? entry.toLowerCase() : entry;
|
||||
return candidate === autoBuildComparator;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { ClaudeCodeVersionInfo, ClaudeInstallationList, ClaudeInstallationI
|
||||
import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths, type ExecFileAsyncOptionsWithVerbatim } from '../cli-tool-manager';
|
||||
import { readSettingsFile, writeSettingsFile } from '../settings-utils';
|
||||
import { isSecurePath } from '../utils/windows-paths';
|
||||
import { isWindows, isMacOS, isLinux } from '../platform';
|
||||
import { getClaudeProfileManager } from '../claude-profile-manager';
|
||||
import { isValidConfigDir } from '../utils/config-path-validator';
|
||||
import { clearKeychainCache } from '../claude-profile/credential-utils';
|
||||
@@ -41,10 +42,8 @@ const VERSION_LIST_CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour for version lis
|
||||
*/
|
||||
async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string | null]> {
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Security validation: reject paths with shell metacharacters or directory traversal
|
||||
if (isWindows && !isSecurePath(cliPath)) {
|
||||
if (isWindows() && !isSecurePath(cliPath)) {
|
||||
throw new Error(`Claude CLI path failed security validation: ${cliPath}`);
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string
|
||||
// /d = disable AutoRun registry commands
|
||||
// /s = strip first and last quotes, preserving inner quotes
|
||||
// /c = run command then terminate
|
||||
if (isWindows && /\.(cmd|bat)$/i.test(cliPath)) {
|
||||
if (isWindows() && /\.(cmd|bat)$/i.test(cliPath)) {
|
||||
// Get cmd.exe path from environment or use default
|
||||
const cmdExe = process.env.ComSpec
|
||||
|| path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
|
||||
@@ -108,7 +107,6 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
|
||||
const installations: ClaudeInstallationInfo[] = [];
|
||||
const seenPaths = new Set<string>();
|
||||
const homeDir = os.homedir();
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Get detection paths from cli-tool-manager (single source of truth)
|
||||
const detectionPaths = getClaudeDetectionPaths(homeDir);
|
||||
@@ -148,7 +146,7 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
|
||||
|
||||
// 2. Check system PATH via which/where
|
||||
try {
|
||||
if (isWindows) {
|
||||
if (isWindows()) {
|
||||
const result = await execFileAsync('where', ['claude'], { timeout: 5000 });
|
||||
const paths = result.stdout.trim().split('\n').filter(p => p.trim());
|
||||
for (const p of paths) {
|
||||
@@ -166,14 +164,14 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
|
||||
}
|
||||
|
||||
// 3. Homebrew paths (macOS) - from getClaudeDetectionPaths
|
||||
if (process.platform === 'darwin') {
|
||||
if (isMacOS()) {
|
||||
for (const p of detectionPaths.homebrewPaths) {
|
||||
await addInstallation(p, 'homebrew');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. NVM paths (Unix) - check Node.js version manager
|
||||
if (!isWindows && existsSync(detectionPaths.nvmVersionsDir)) {
|
||||
if (!isWindows() && existsSync(detectionPaths.nvmVersionsDir)) {
|
||||
try {
|
||||
const entries = await fsPromises.readdir(detectionPaths.nvmVersionsDir, { withFileTypes: true });
|
||||
const versionDirs = sortNvmVersionDirs(entries);
|
||||
@@ -192,7 +190,7 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
|
||||
}
|
||||
|
||||
// 6. Additional common paths not in getClaudeDetectionPaths (for broader scanning)
|
||||
const additionalPaths = isWindows
|
||||
const additionalPaths = isWindows()
|
||||
? [] // Windows paths are well covered by detectionPaths.platformPaths
|
||||
: [
|
||||
path.join(homeDir, '.npm-global', 'bin', 'claude'),
|
||||
@@ -338,7 +336,7 @@ async function fetchAvailableVersions(): Promise<string[]> {
|
||||
* @param version - The version to install (e.g., "1.0.5")
|
||||
*/
|
||||
function getInstallVersionCommand(version: string): string {
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
// Windows: kill running Claude processes first, then install specific version
|
||||
return `taskkill /IM claude.exe /F 2>nul; claude install --force ${version}`;
|
||||
} else {
|
||||
@@ -352,7 +350,7 @@ function getInstallVersionCommand(version: string): string {
|
||||
* @param isUpdate - If true, Claude is already installed and we just need to update
|
||||
*/
|
||||
function getInstallCommand(isUpdate: boolean): string {
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
if (isUpdate) {
|
||||
// Update: kill running Claude processes first, then update with --force
|
||||
return 'taskkill /IM claude.exe /F 2>nul; claude install --force latest';
|
||||
@@ -435,14 +433,13 @@ export function escapeBashCommand(str: string): string {
|
||||
* Supports macOS, Windows, and Linux terminals
|
||||
*/
|
||||
export async function openTerminalWithCommand(command: string): Promise<void> {
|
||||
const platform = process.platform;
|
||||
const settings = readSettingsFile();
|
||||
const preferredTerminal = settings?.preferredTerminal as string | undefined;
|
||||
|
||||
console.warn('[Claude Code] Platform:', platform);
|
||||
console.warn('[Claude Code] Platform:', isWindows() ? 'Windows' : isMacOS() ? 'macOS' : 'Linux');
|
||||
console.warn('[Claude Code] Preferred terminal:', preferredTerminal);
|
||||
|
||||
if (platform === 'darwin') {
|
||||
if (isMacOS()) {
|
||||
// macOS: Use AppleScript to open terminal with command
|
||||
const escapedCommand = escapeAppleScriptString(command);
|
||||
let script: string;
|
||||
@@ -551,7 +548,7 @@ export async function openTerminalWithCommand(command: string): Promise<void> {
|
||||
console.warn('[Claude Code] Running AppleScript...');
|
||||
execFileSync('osascript', ['-e', script], { stdio: 'pipe' });
|
||||
|
||||
} else if (platform === 'win32') {
|
||||
} else if (isWindows()) {
|
||||
// Windows: Use appropriate terminal
|
||||
// Values match SupportedTerminal type: 'windowsterminal', 'powershell', 'cmd', 'conemu', 'cmder',
|
||||
// 'gitbash', 'alacritty', 'wezterm', 'hyper', 'tabby', 'cygwin', 'msys2'
|
||||
@@ -875,7 +872,7 @@ function checkProfileAuthentication(configDir: string): AuthCheckResult {
|
||||
}
|
||||
|
||||
// On Linux, also check .credentials.json (Claude CLI may store tokens here)
|
||||
if (process.platform === 'linux' && existsSync(credentialsJsonPath)) {
|
||||
if (isLinux() && existsSync(credentialsJsonPath)) {
|
||||
const content = readFileSync(credentialsJsonPath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ import { projectStore } from '../../project-store';
|
||||
import { changelogService } from '../../changelog-service';
|
||||
import type { ReleaseOptions } from './types';
|
||||
import { getToolPath } from '../../cli-tool-manager';
|
||||
import { getWhichCommand } from '../../platform';
|
||||
|
||||
/**
|
||||
* Check if gh CLI is installed
|
||||
*/
|
||||
function checkGhCli(): { installed: boolean; error?: string } {
|
||||
try {
|
||||
const checkCmd = process.platform === 'win32' ? 'where gh' : 'which gh';
|
||||
const checkCmd = `${getWhichCommand()} gh`;
|
||||
execSync(checkCmd, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
return { installed: true };
|
||||
} catch {
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { AuthFailureInfo } from '../../../../shared/types/terminal';
|
||||
import { parsePythonCommand } from '../../../python-detector';
|
||||
import { detectAuthFailure } from '../../../rate-limit-detector';
|
||||
import { getClaudeProfileManager } from '../../../claude-profile-manager';
|
||||
import { isWindows } from '../../../platform';
|
||||
import { isWindows, isMacOS } from '../../../platform';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -451,9 +451,9 @@ export async function validateGitHubModule(project: Project): Promise<GitHubModu
|
||||
result.ghCliInstalled = true;
|
||||
} catch {
|
||||
result.ghCliInstalled = false;
|
||||
const installInstructions = process.platform === 'win32'
|
||||
const installInstructions = isWindows()
|
||||
? 'winget install --id GitHub.cli'
|
||||
: process.platform === 'darwin'
|
||||
: isMacOS()
|
||||
? 'brew install gh'
|
||||
: 'See https://cli.github.com/';
|
||||
result.error = `GitHub CLI (gh) is not installed. Install it with:\n ${installInstructions}`;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IPC_CHANNELS } from '../../shared/constants/ipc';
|
||||
import type { CustomMcpServer, McpHealthCheckResult, McpHealthStatus, McpTestConnectionResult } from '../../shared/types/project';
|
||||
import { spawn } from 'child_process';
|
||||
import { appLog } from '../app-logger';
|
||||
import { isWindows } from '../platform';
|
||||
|
||||
/**
|
||||
* Defense-in-depth: Frontend-side command validation
|
||||
@@ -54,7 +55,7 @@ function areArgsSafe(args: string[] | undefined): boolean {
|
||||
if (args.some(arg => DANGEROUS_FLAGS.has(arg))) return false;
|
||||
|
||||
// On Windows with shell: true, check for shell metacharacters that could enable injection
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
if (args.some(arg => SHELL_METACHARACTERS.some(char => arg.includes(char)))) {
|
||||
return false;
|
||||
}
|
||||
@@ -193,7 +194,7 @@ async function checkCommandHealth(server: CustomMcpServer, startTime: number): P
|
||||
});
|
||||
}
|
||||
|
||||
const command = process.platform === 'win32' ? 'where' : 'which';
|
||||
const command = isWindows() ? 'where' : 'which';
|
||||
const proc = spawn(command, [server.command!], {
|
||||
timeout: 5000,
|
||||
});
|
||||
@@ -421,7 +422,7 @@ async function testCommandConnection(server: CustomMcpServer, startTime: number)
|
||||
const proc = spawn(server.command!, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 15000, // OS-level timeout for reliable process termination
|
||||
shell: process.platform === 'win32', // Required for Windows to run npx.cmd
|
||||
shell: isWindows(), // Required for Windows to run npx.cmd
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
|
||||
@@ -10,7 +10,7 @@ import { spawn, execFileSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { getOllamaExecutablePaths, getOllamaInstallCommand as getPlatformOllamaInstallCommand, getWhichCommand, getCurrentOS } from '../platform';
|
||||
|
||||
// ESM-compatible __dirname
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -109,38 +109,11 @@ interface OllamaInstallStatus {
|
||||
* @returns {OllamaInstallStatus} Installation status with path if found
|
||||
*/
|
||||
function checkOllamaInstalled(): OllamaInstallStatus {
|
||||
const platform = process.platform;
|
||||
|
||||
// Common paths to check based on platform
|
||||
const pathsToCheck: string[] = [];
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Windows: Check common installation paths
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||
pathsToCheck.push(
|
||||
path.join(localAppData, 'Programs', 'Ollama', 'ollama.exe'),
|
||||
path.join(localAppData, 'Ollama', 'ollama.exe'),
|
||||
'C:\\Program Files\\Ollama\\ollama.exe',
|
||||
'C:\\Program Files (x86)\\Ollama\\ollama.exe'
|
||||
);
|
||||
} else if (platform === 'darwin') {
|
||||
// macOS: Check common paths
|
||||
pathsToCheck.push(
|
||||
'/usr/local/bin/ollama',
|
||||
'/opt/homebrew/bin/ollama',
|
||||
path.join(os.homedir(), '.local', 'bin', 'ollama')
|
||||
);
|
||||
} else {
|
||||
// Linux: Check common paths
|
||||
pathsToCheck.push(
|
||||
'/usr/local/bin/ollama',
|
||||
'/usr/bin/ollama',
|
||||
path.join(os.homedir(), '.local', 'bin', 'ollama')
|
||||
);
|
||||
}
|
||||
// Get platform-specific paths from the platform module
|
||||
const pathsToCheck = getOllamaExecutablePaths();
|
||||
|
||||
// Check each path
|
||||
// SECURITY NOTE: ollamaPath values come from the hardcoded pathsToCheck array above,
|
||||
// SECURITY NOTE: ollamaPath values come from the platform module's hardcoded paths,
|
||||
// not from user input or environment variables. These are known system installation paths.
|
||||
for (const ollamaPath of pathsToCheck) {
|
||||
if (fs.existsSync(ollamaPath)) {
|
||||
@@ -172,7 +145,7 @@ function checkOllamaInstalled(): OllamaInstallStatus {
|
||||
// Also check if ollama is in PATH using where/which command
|
||||
// Use execFileSync with explicit command to avoid shell injection
|
||||
try {
|
||||
const whichCmd = platform === 'win32' ? 'where.exe' : 'which';
|
||||
const whichCmd = getWhichCommand();
|
||||
const ollamaPath = execFileSync(whichCmd, ['ollama'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
@@ -211,36 +184,16 @@ function checkOllamaInstalled(): OllamaInstallStatus {
|
||||
|
||||
/**
|
||||
* Get the platform-specific install command for Ollama
|
||||
* Uses the official Ollama installation methods
|
||||
* Uses the official Ollama installation methods from the platform module.
|
||||
*
|
||||
* Windows: Uses winget (Windows Package Manager)
|
||||
* - Official method per https://winstall.app/apps/Ollama.Ollama
|
||||
* - Winget is pre-installed on Windows 10 (1709+) and Windows 11
|
||||
*
|
||||
* macOS: Uses Homebrew (most common package manager on macOS)
|
||||
* - Official method: brew install ollama
|
||||
* - Reference: https://ollama.com/download/mac
|
||||
*
|
||||
* macOS: Uses Homebrew
|
||||
* Linux: Uses official install script from https://ollama.com/download
|
||||
*
|
||||
* @returns {string} The install command to run in terminal
|
||||
*/
|
||||
function getOllamaInstallCommand(): string {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: Use winget (Windows Package Manager)
|
||||
// This is an official installation method for Ollama on Windows
|
||||
// Reference: https://winstall.app/apps/Ollama.Ollama
|
||||
return 'winget install --id Ollama.Ollama --accept-source-agreements';
|
||||
} else if (process.platform === 'darwin') {
|
||||
// macOS: Use Homebrew (most widely used package manager on macOS)
|
||||
// Official Ollama installation method for macOS
|
||||
// Reference: https://ollama.com/download/mac
|
||||
return 'brew install ollama';
|
||||
} else {
|
||||
// Linux: Use shell script from official Ollama
|
||||
// Reference: https://ollama.com/download
|
||||
return 'curl -fsSL https://ollama.com/install.sh | sh';
|
||||
}
|
||||
return getPlatformOllamaInstallCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,7 +568,7 @@ export function registerMemoryHandlers(): void {
|
||||
async (): Promise<IPCResult<{ command: string }>> => {
|
||||
try {
|
||||
const command = getOllamaInstallCommand();
|
||||
console.log('[Ollama] Platform:', process.platform);
|
||||
console.log('[Ollama] Platform:', getCurrentOS());
|
||||
console.log('[Ollama] Install command:', command);
|
||||
console.log('[Ollama] Opening terminal...');
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { minimatch } from 'minimatch';
|
||||
import { debugLog, debugError } from '../../../shared/utils/debug-logger';
|
||||
import { projectStore } from '../../project-store';
|
||||
import { parseEnvFile } from '../utils';
|
||||
import { isWindows } from '../../platform';
|
||||
import {
|
||||
getTerminalWorktreeDir,
|
||||
getTerminalWorktreePath,
|
||||
@@ -276,7 +277,7 @@ function symlinkNodeModulesToWorktree(projectPath: string, worktreePath: string)
|
||||
// Platform-specific symlink creation:
|
||||
// - Windows: Use 'junction' type which requires absolute paths (no admin rights required)
|
||||
// - Unix (macOS/Linux): Use relative paths for portability (worktree can be moved)
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
symlinkSync(sourcePath, targetPath, 'junction');
|
||||
debugLog('[TerminalWorktree] Created junction (Windows):', targetRel, '->', sourcePath);
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@ const __dirname = path.dirname(__filename);
|
||||
import { findPythonCommand, parsePythonCommand } from './python-detector';
|
||||
import { getConfiguredPythonPath, pythonEnvManager } from './python-env-manager';
|
||||
import { getMemoriesDir } from './config-paths';
|
||||
import { isWindows } from './platform';
|
||||
import type { MemoryEpisode } from '../shared/types';
|
||||
|
||||
interface MemoryServiceConfig {
|
||||
@@ -141,7 +142,7 @@ function getBackendPythonPath(): string {
|
||||
|
||||
for (const backendPath of possibleBackendPaths) {
|
||||
// Check for backend venv Python (has real_ladybug installed)
|
||||
const venvPython = process.platform === 'win32'
|
||||
const venvPython = isWindows()
|
||||
? path.join(backendPath, '.venv', 'Scripts', 'python.exe')
|
||||
: path.join(backendPath, '.venv', 'bin', 'python');
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { spawn, ChildProcess } from 'child_process';
|
||||
import { OS, ShellType, PathConfig, ShellConfig, BinaryDirectories } from './types';
|
||||
|
||||
// Re-export from paths.ts for backward compatibility
|
||||
export { getWindowsShellPaths } from './paths';
|
||||
export { getWindowsShellPaths, getOllamaExecutablePaths, getOllamaInstallCommand, getWhichCommand } from './paths';
|
||||
|
||||
/**
|
||||
* Get the current operating system
|
||||
|
||||
@@ -239,6 +239,71 @@ export function expandWindowsEnvVars(pathPattern: string): string {
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Ollama executable paths
|
||||
*
|
||||
* Returns platform-specific paths where Ollama may be installed:
|
||||
* - Windows: LocalAppData, Program Files
|
||||
* - macOS: Homebrew paths, /usr/local/bin
|
||||
* - Linux: /usr/local/bin, /usr/bin, ~/.local/bin
|
||||
*/
|
||||
export function getOllamaExecutablePaths(): string[] {
|
||||
const homeDir = os.homedir();
|
||||
const paths: string[] = [];
|
||||
|
||||
if (isWindows()) {
|
||||
const localAppData = process.env.LOCALAPPDATA || joinPaths(homeDir, 'AppData', 'Local');
|
||||
paths.push(
|
||||
joinPaths(localAppData, 'Programs', 'Ollama', 'ollama.exe'),
|
||||
joinPaths(localAppData, 'Ollama', 'ollama.exe'),
|
||||
joinPaths('C:\\Program Files', 'Ollama', 'ollama.exe'),
|
||||
joinPaths('C:\\Program Files (x86)', 'Ollama', 'ollama.exe')
|
||||
);
|
||||
} else if (isMacOS()) {
|
||||
paths.push(
|
||||
'/usr/local/bin/ollama',
|
||||
'/opt/homebrew/bin/ollama',
|
||||
joinPaths(homeDir, '.local', 'bin', 'ollama')
|
||||
);
|
||||
} else {
|
||||
// Linux
|
||||
paths.push(
|
||||
'/usr/local/bin/ollama',
|
||||
'/usr/bin/ollama',
|
||||
joinPaths(homeDir, '.local', 'bin', 'ollama')
|
||||
);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform-specific install command for Ollama
|
||||
*
|
||||
* Windows: Uses winget (Windows Package Manager)
|
||||
* macOS: Uses Homebrew
|
||||
* Linux: Uses official install script
|
||||
*/
|
||||
export function getOllamaInstallCommand(): string {
|
||||
if (isWindows()) {
|
||||
return 'winget install --id Ollama.Ollama --accept-source-agreements';
|
||||
} else if (isMacOS()) {
|
||||
return 'brew install ollama';
|
||||
} else {
|
||||
return 'curl -fsSL https://ollama.com/install.sh | sh';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command to find executables in PATH
|
||||
*
|
||||
* Windows: where.exe
|
||||
* Unix: which
|
||||
*/
|
||||
export function getWhichCommand(): string {
|
||||
return isWindows() ? 'where.exe' : 'which';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Windows-specific installation paths for a tool
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ import { existsSync, accessSync, constants } from 'fs';
|
||||
import path from 'path';
|
||||
import { app } from 'electron';
|
||||
import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-python';
|
||||
import { isWindows } from './platform';
|
||||
|
||||
/**
|
||||
* Get the path to the bundled Python executable.
|
||||
@@ -17,10 +18,9 @@ export function getBundledPythonPath(): string | null {
|
||||
}
|
||||
|
||||
const resourcesPath = process.resourcesPath;
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Bundled Python location in packaged app
|
||||
const pythonPath = isWindows
|
||||
const pythonPath = isWindows()
|
||||
? path.join(resourcesPath, 'python', 'python.exe')
|
||||
: path.join(resourcesPath, 'python', 'bin', 'python3');
|
||||
|
||||
@@ -52,8 +52,6 @@ function findHomebrewPython(): string | null {
|
||||
* @returns The Python command to use, or null if none found
|
||||
*/
|
||||
export function findPythonCommand(): string | null {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// 1. Check for bundled Python first (packaged apps only)
|
||||
const bundledPython = getBundledPythonPath();
|
||||
if (bundledPython) {
|
||||
@@ -75,7 +73,7 @@ export function findPythonCommand(): string | null {
|
||||
|
||||
// Build candidate list prioritizing Homebrew Python on macOS
|
||||
let candidates: string[];
|
||||
if (isWindows) {
|
||||
if (isWindows()) {
|
||||
candidates = ['py -3', 'python', 'python3', 'py'];
|
||||
} else {
|
||||
const homebrewPython = findHomebrewPython();
|
||||
@@ -101,7 +99,7 @@ export function findPythonCommand(): string | null {
|
||||
}
|
||||
|
||||
// Fallback to platform-specific default
|
||||
if (isWindows) {
|
||||
if (isWindows()) {
|
||||
return 'python';
|
||||
}
|
||||
return findHomebrewPython() || 'python3';
|
||||
@@ -186,7 +184,7 @@ export function getDefaultPythonCommand(): string {
|
||||
}
|
||||
|
||||
// Fall back to system Python
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows()) {
|
||||
return 'python';
|
||||
}
|
||||
return findHomebrewPython() || 'python3';
|
||||
@@ -441,7 +439,7 @@ export function validatePythonPath(pythonPath: string): PythonPathValidation {
|
||||
}
|
||||
|
||||
// Security check 4: Must be executable (Unix) or .exe (Windows)
|
||||
if (process.platform !== 'win32' && !isExecutable(normalizedPath)) {
|
||||
if (!isWindows() && !isExecutable(normalizedPath)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'File exists but is not executable'
|
||||
|
||||
@@ -16,10 +16,9 @@ import { isWindows, GRACEFUL_KILL_TIMEOUT_MS } from '../platform';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SOCKET_PATH =
|
||||
process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\auto-claude-pty-${process.getuid?.() || 'default'}`
|
||||
: `/tmp/auto-claude-pty-${process.getuid?.() || 'default'}.sock`;
|
||||
const SOCKET_PATH = isWindows()
|
||||
? `\\\\.\\pipe\\auto-claude-pty-${process.getuid?.() || 'default'}`
|
||||
: `/tmp/auto-claude-pty-${process.getuid?.() || 'default'}.sock`;
|
||||
|
||||
interface DaemonResponseData {
|
||||
exitCode?: number;
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
import * as net from 'net';
|
||||
import * as fs from 'fs';
|
||||
import * as pty from '@lydell/node-pty';
|
||||
import { isWindows, isUnix } from '../platform';
|
||||
|
||||
const SOCKET_PATH =
|
||||
process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\auto-claude-pty-${process.getuid?.() || 'default'}`
|
||||
: `/tmp/auto-claude-pty-${process.getuid?.() || 'default'}.sock`;
|
||||
const SOCKET_PATH = isWindows()
|
||||
? `\\\\.\\pipe\\auto-claude-pty-${process.getuid?.() || 'default'}`
|
||||
: `/tmp/auto-claude-pty-${process.getuid?.() || 'default'}.sock`;
|
||||
|
||||
// Maximum buffer size per PTY (100KB)
|
||||
const MAX_BUFFER_SIZE = 100_000;
|
||||
@@ -83,7 +83,7 @@ class PtyDaemon {
|
||||
* Remove stale socket/pipe
|
||||
*/
|
||||
private cleanup(): void {
|
||||
if (process.platform !== 'win32' && fs.existsSync(SOCKET_PATH)) {
|
||||
if (isUnix() && fs.existsSync(SOCKET_PATH)) {
|
||||
try {
|
||||
fs.unlinkSync(SOCKET_PATH);
|
||||
console.error('[PTY Daemon] Cleaned up stale socket');
|
||||
@@ -113,7 +113,7 @@ class PtyDaemon {
|
||||
this.server.listen(SOCKET_PATH, () => {
|
||||
console.error(`[PTY Daemon] Listening on ${SOCKET_PATH}`);
|
||||
// Set permissions on Unix
|
||||
if (process.platform !== 'win32') {
|
||||
if (isUnix()) {
|
||||
try {
|
||||
fs.chmodSync(SOCKET_PATH, 0o600);
|
||||
} catch (error) {
|
||||
|
||||
@@ -101,7 +101,7 @@ def setup_qa_report_mocks() -> None:
|
||||
sys.modules['client'] = mock_client
|
||||
|
||||
# Add auto-claude path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
|
||||
def cleanup_qa_report_mocks() -> None:
|
||||
|
||||
@@ -17,7 +17,7 @@ import sys
|
||||
import json
|
||||
|
||||
# Add parent directory to path to import analyzer
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from analyzer import ServiceAnalyzer
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from ci_discovery import (
|
||||
CIConfig,
|
||||
|
||||
@@ -13,7 +13,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from critique import (
|
||||
generate_critique_prompt,
|
||||
@@ -22,7 +22,7 @@ from critique import (
|
||||
format_critique_summary,
|
||||
CritiqueResult,
|
||||
)
|
||||
from implementation_plan import Chunk, ChunkStatus, Verification, VerificationType
|
||||
from implementation_plan import Subtask, SubtaskStatus, Verification, VerificationType
|
||||
|
||||
|
||||
def test_critique_data_structures():
|
||||
@@ -134,14 +134,14 @@ def test_critique_response_parsing():
|
||||
|
||||
|
||||
def test_implementation_plan_integration():
|
||||
"""Test integration with implementation_plan.py Chunk class."""
|
||||
"""Test integration with implementation_plan.py Subtask class."""
|
||||
print("\nTesting implementation plan integration...")
|
||||
|
||||
# Create a chunk with critique result
|
||||
chunk = Chunk(
|
||||
chunk = Subtask(
|
||||
id="test-chunk",
|
||||
description="Test chunk with critique",
|
||||
status=ChunkStatus.PENDING,
|
||||
status=SubtaskStatus.PENDING,
|
||||
service="backend",
|
||||
files_to_modify=["app/test.py"],
|
||||
)
|
||||
@@ -161,7 +161,7 @@ def test_implementation_plan_integration():
|
||||
assert chunk_dict["critique_result"]["passes"] == True
|
||||
|
||||
# Test deserialization
|
||||
chunk2 = Chunk.from_dict(chunk_dict)
|
||||
chunk2 = Subtask.from_dict(chunk_dict)
|
||||
assert chunk2.critique_result is not None
|
||||
assert chunk2.critique_result["passes"] == True
|
||||
assert len(chunk2.critique_result["improvements_made"]) == 1
|
||||
@@ -218,7 +218,7 @@ def test_complete_workflow():
|
||||
assert "Subtask is ready to be marked complete" in summary
|
||||
|
||||
# 7. Store in chunk
|
||||
chunk_obj = Chunk(
|
||||
chunk_obj = Subtask(
|
||||
id=chunk["id"],
|
||||
description=chunk["description"],
|
||||
service=chunk["service"],
|
||||
@@ -286,7 +286,7 @@ def main():
|
||||
print("\nKey components:")
|
||||
print(" - critique.py: Core critique logic")
|
||||
print(" - prompts/coder.md: Updated with STEP 6.5 (mandatory critique)")
|
||||
print(" - implementation_plan.py: Chunk.critique_result field added")
|
||||
print(" - implementation_plan.py: Subtask.critique_result field added")
|
||||
|
||||
except AssertionError as e:
|
||||
print(f"\n✗ Test failed: {e}")
|
||||
|
||||
@@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from test_discovery import (
|
||||
TestFramework,
|
||||
|
||||
+43
-43
@@ -16,8 +16,8 @@ from pathlib import Path
|
||||
from implementation_plan import (
|
||||
ImplementationPlan,
|
||||
Phase,
|
||||
Chunk,
|
||||
ChunkStatus,
|
||||
Subtask,
|
||||
SubtaskStatus,
|
||||
PhaseType,
|
||||
WorkflowType,
|
||||
)
|
||||
@@ -31,8 +31,8 @@ class TestAddFollowupPhase:
|
||||
plan = ImplementationPlan(feature="Test Feature")
|
||||
|
||||
new_chunks = [
|
||||
Chunk(id="followup-1", description="First follow-up task"),
|
||||
Chunk(id="followup-2", description="Second follow-up task"),
|
||||
Subtask(id="followup-1", description="First follow-up task"),
|
||||
Subtask(id="followup-2", description="Second follow-up task"),
|
||||
]
|
||||
|
||||
phase = plan.add_followup_phase("Follow-Up: New Work", new_chunks)
|
||||
@@ -53,7 +53,7 @@ class TestAddFollowupPhase:
|
||||
],
|
||||
)
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="Follow-up task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="Follow-up task")]
|
||||
phase = plan.add_followup_phase("Follow-Up Phase", new_chunks)
|
||||
|
||||
assert phase.phase == 3
|
||||
@@ -70,7 +70,7 @@ class TestAddFollowupPhase:
|
||||
],
|
||||
)
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="Follow-up task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="Follow-up task")]
|
||||
phase = plan.add_followup_phase("Follow-Up Phase", new_chunks)
|
||||
|
||||
assert phase.depends_on == [1, 2, 3]
|
||||
@@ -79,7 +79,7 @@ class TestAddFollowupPhase:
|
||||
"""Respects phase_type parameter."""
|
||||
plan = ImplementationPlan(feature="Test Feature")
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="Integration task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="Integration task")]
|
||||
phase = plan.add_followup_phase(
|
||||
"Integration Work",
|
||||
new_chunks,
|
||||
@@ -92,7 +92,7 @@ class TestAddFollowupPhase:
|
||||
"""Respects parallel_safe parameter."""
|
||||
plan = ImplementationPlan(feature="Test Feature")
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="Parallel task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="Parallel task")]
|
||||
phase = plan.add_followup_phase(
|
||||
"Parallel Work",
|
||||
new_chunks,
|
||||
@@ -109,7 +109,7 @@ class TestAddFollowupPhase:
|
||||
planStatus="completed",
|
||||
)
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="New task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="New task")]
|
||||
plan.add_followup_phase("Follow-Up", new_chunks)
|
||||
|
||||
assert plan.status == "in_progress"
|
||||
@@ -122,7 +122,7 @@ class TestAddFollowupPhase:
|
||||
qa_signoff={"status": "approved", "timestamp": "2024-01-01"},
|
||||
)
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="New task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="New task")]
|
||||
plan.add_followup_phase("Follow-Up", new_chunks)
|
||||
|
||||
assert plan.qa_signoff is None
|
||||
@@ -131,7 +131,7 @@ class TestAddFollowupPhase:
|
||||
"""Returns the newly created Phase object."""
|
||||
plan = ImplementationPlan(feature="Test Feature")
|
||||
|
||||
new_chunks = [Chunk(id="followup-1", description="New task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="New task")]
|
||||
phase = plan.add_followup_phase("Follow-Up", new_chunks)
|
||||
|
||||
assert isinstance(phase, Phase)
|
||||
@@ -146,11 +146,11 @@ class TestAddFollowupPhase:
|
||||
)
|
||||
|
||||
# First follow-up
|
||||
plan.add_followup_phase("Follow-Up 1", [Chunk(id="f1", description="Task 1")])
|
||||
plan.add_followup_phase("Follow-Up 1", [Subtask(id="f1", description="Task 1")])
|
||||
# Second follow-up
|
||||
plan.add_followup_phase("Follow-Up 2", [Chunk(id="f2", description="Task 2")])
|
||||
plan.add_followup_phase("Follow-Up 2", [Subtask(id="f2", description="Task 2")])
|
||||
# Third follow-up
|
||||
plan.add_followup_phase("Follow-Up 3", [Chunk(id="f3", description="Task 3")])
|
||||
plan.add_followup_phase("Follow-Up 3", [Subtask(id="f3", description="Task 3")])
|
||||
|
||||
assert len(plan.phases) == 4
|
||||
assert plan.phases[0].phase == 1
|
||||
@@ -163,13 +163,13 @@ class TestAddFollowupPhase:
|
||||
plan = ImplementationPlan(feature="Test Feature")
|
||||
|
||||
new_chunks = [
|
||||
Chunk(id="followup-1", description="Task 1"),
|
||||
Chunk(id="followup-2", description="Task 2"),
|
||||
Subtask(id="followup-1", description="Task 1"),
|
||||
Subtask(id="followup-2", description="Task 2"),
|
||||
]
|
||||
phase = plan.add_followup_phase("Follow-Up", new_chunks)
|
||||
|
||||
for chunk in phase.chunks:
|
||||
assert chunk.status == ChunkStatus.PENDING
|
||||
assert chunk.status == SubtaskStatus.PENDING
|
||||
|
||||
|
||||
class TestResetForFollowup:
|
||||
@@ -185,7 +185,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -206,7 +206,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -227,7 +227,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -249,8 +249,8 @@ class TestResetForFollowup:
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[
|
||||
Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED),
|
||||
Chunk(id="c2", description="Task 2", status=ChunkStatus.COMPLETED),
|
||||
Subtask(id="c1", description="Task 1", status=SubtaskStatus.COMPLETED),
|
||||
Subtask(id="c2", description="Task 2", status=SubtaskStatus.COMPLETED),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -272,8 +272,8 @@ class TestResetForFollowup:
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[
|
||||
Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED),
|
||||
Chunk(id="c2", description="Task 2", status=ChunkStatus.PENDING),
|
||||
Subtask(id="c1", description="Task 1", status=SubtaskStatus.COMPLETED),
|
||||
Subtask(id="c2", description="Task 2", status=SubtaskStatus.PENDING),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -293,7 +293,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.PENDING)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.PENDING)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -313,7 +313,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -333,7 +333,7 @@ class TestResetForFollowup:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -357,10 +357,10 @@ class TestExistingChunksPreserved:
|
||||
phase=1,
|
||||
name="Original Phase",
|
||||
subtasks=[
|
||||
Chunk(
|
||||
Subtask(
|
||||
id="original-1",
|
||||
description="Original task",
|
||||
status=ChunkStatus.COMPLETED,
|
||||
status=SubtaskStatus.COMPLETED,
|
||||
completed_at="2024-01-01T12:00:00",
|
||||
),
|
||||
],
|
||||
@@ -369,12 +369,12 @@ class TestExistingChunksPreserved:
|
||||
)
|
||||
|
||||
# Add follow-up
|
||||
new_chunks = [Chunk(id="followup-1", description="New task")]
|
||||
new_chunks = [Subtask(id="followup-1", description="New task")]
|
||||
plan.add_followup_phase("Follow-Up", new_chunks)
|
||||
|
||||
# Original chunk should still be completed
|
||||
original_chunk = plan.phases[0].chunks[0]
|
||||
assert original_chunk.status == ChunkStatus.COMPLETED
|
||||
assert original_chunk.status == SubtaskStatus.COMPLETED
|
||||
assert original_chunk.completed_at == "2024-01-01T12:00:00"
|
||||
|
||||
def test_original_phase_structure_preserved(self):
|
||||
@@ -384,13 +384,13 @@ class TestExistingChunksPreserved:
|
||||
phase=1,
|
||||
name="Phase 1",
|
||||
depends_on=[],
|
||||
subtasks=[Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task 1", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
Phase(
|
||||
phase=2,
|
||||
name="Phase 2",
|
||||
depends_on=[1],
|
||||
subtasks=[Chunk(id="c2", description="Task 2", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c2", description="Task 2", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -399,7 +399,7 @@ class TestExistingChunksPreserved:
|
||||
phases=original_phases,
|
||||
)
|
||||
|
||||
plan.add_followup_phase("Follow-Up", [Chunk(id="f1", description="Follow-up")])
|
||||
plan.add_followup_phase("Follow-Up", [Subtask(id="f1", description="Follow-up")])
|
||||
|
||||
# Original phases should be unchanged
|
||||
assert plan.phases[0].name == "Phase 1"
|
||||
@@ -420,7 +420,7 @@ class TestFollowupPlanSaveLoad:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Original",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -428,7 +428,7 @@ class TestFollowupPlanSaveLoad:
|
||||
# Add follow-up
|
||||
plan.add_followup_phase(
|
||||
"Follow-Up Work",
|
||||
[Chunk(id="followup-1", description="Follow-up task")],
|
||||
[Subtask(id="followup-1", description="Follow-up task")],
|
||||
)
|
||||
|
||||
# Save
|
||||
@@ -451,7 +451,7 @@ class TestFollowupPlanSaveLoad:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Original",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -459,12 +459,12 @@ class TestFollowupPlanSaveLoad:
|
||||
plan_path = temp_dir / "implementation_plan.json"
|
||||
|
||||
# Add first follow-up and save
|
||||
plan.add_followup_phase("Follow-Up 1", [Chunk(id="f1", description="Task 1")])
|
||||
plan.add_followup_phase("Follow-Up 1", [Subtask(id="f1", description="Task 1")])
|
||||
plan.save(plan_path)
|
||||
|
||||
# Load, add second follow-up, save
|
||||
plan = ImplementationPlan.load(plan_path)
|
||||
plan.add_followup_phase("Follow-Up 2", [Chunk(id="f2", description="Task 2")])
|
||||
plan.add_followup_phase("Follow-Up 2", [Subtask(id="f2", description="Task 2")])
|
||||
plan.save(plan_path)
|
||||
|
||||
# Load and verify
|
||||
@@ -487,7 +487,7 @@ class TestFollowupProgressCalculation:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Original",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -499,7 +499,7 @@ class TestFollowupProgressCalculation:
|
||||
assert progress["is_complete"] is True
|
||||
|
||||
# Add follow-up
|
||||
plan.add_followup_phase("Follow-Up", [Chunk(id="f1", description="New task")])
|
||||
plan.add_followup_phase("Follow-Up", [Subtask(id="f1", description="New task")])
|
||||
|
||||
# Now 50% complete
|
||||
progress = plan.get_progress()
|
||||
@@ -516,7 +516,7 @@ class TestFollowupProgressCalculation:
|
||||
Phase(
|
||||
phase=1,
|
||||
name="Original",
|
||||
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
|
||||
subtasks=[Subtask(id="c1", description="Task", status=SubtaskStatus.COMPLETED)],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -525,7 +525,7 @@ class TestFollowupProgressCalculation:
|
||||
assert plan.get_next_subtask() is None
|
||||
|
||||
# Add follow-up
|
||||
plan.add_followup_phase("Follow-Up", [Chunk(id="f1", description="New task")])
|
||||
plan.add_followup_phase("Follow-Up", [Subtask(id="f1", description="New task")])
|
||||
|
||||
# Now follow-up chunk is next
|
||||
next_work = plan.get_next_subtask()
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add auto-claude to path
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from graphiti_config import is_graphiti_enabled, get_graphiti_status, GraphitiConfig
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests for Implementation Plan Management
|
||||
========================================
|
||||
|
||||
Tests the implementation_plan.py module functionality including:
|
||||
- Data structures (Chunk, Phase, ImplementationPlan)
|
||||
- Data structures (Subtask, Phase, ImplementationPlan)
|
||||
- Status transitions
|
||||
- Progress tracking
|
||||
- Dependency resolution
|
||||
@@ -19,11 +19,11 @@ from pathlib import Path
|
||||
from implementation_plan import (
|
||||
ImplementationPlan,
|
||||
Phase,
|
||||
Chunk,
|
||||
Subtask,
|
||||
Verification,
|
||||
WorkflowType,
|
||||
PhaseType,
|
||||
ChunkStatus,
|
||||
SubtaskStatus,
|
||||
VerificationType,
|
||||
create_feature_plan,
|
||||
create_investigation_plan,
|
||||
@@ -31,29 +31,29 @@ from implementation_plan import (
|
||||
)
|
||||
|
||||
|
||||
class TestChunk:
|
||||
"""Tests for Chunk data structure."""
|
||||
class TestSubtask:
|
||||
"""Tests for Subtask data structure."""
|
||||
|
||||
def test_create_simple_chunk(self):
|
||||
"""Creates a simple chunk with defaults."""
|
||||
chunk = Chunk(
|
||||
chunk = Subtask(
|
||||
id="chunk-1",
|
||||
description="Implement user model",
|
||||
)
|
||||
|
||||
assert chunk.id == "chunk-1"
|
||||
assert chunk.description == "Implement user model"
|
||||
assert chunk.status == ChunkStatus.PENDING
|
||||
assert chunk.status == SubtaskStatus.PENDING
|
||||
assert chunk.service is None
|
||||
assert chunk.files_to_modify == []
|
||||
assert chunk.files_to_create == []
|
||||
|
||||
def test_create_full_chunk(self):
|
||||
"""Creates a chunk with all fields."""
|
||||
chunk = Chunk(
|
||||
chunk = Subtask(
|
||||
id="chunk-2",
|
||||
description="Add API endpoint",
|
||||
status=ChunkStatus.IN_PROGRESS,
|
||||
status=SubtaskStatus.IN_PROGRESS,
|
||||
service="backend",
|
||||
files_to_modify=["app/routes.py"],
|
||||
files_to_create=["app/models/user.py"],
|
||||
@@ -65,39 +65,39 @@ class TestChunk:
|
||||
assert "app/models/user.py" in chunk.files_to_create
|
||||
|
||||
def test_chunk_start(self):
|
||||
"""Chunk can be started."""
|
||||
chunk = Chunk(id="test", description="Test")
|
||||
"""Subtask can be started."""
|
||||
chunk = Subtask(id="test", description="Test")
|
||||
|
||||
chunk.start(session_id=1)
|
||||
|
||||
assert chunk.status == ChunkStatus.IN_PROGRESS
|
||||
assert chunk.status == SubtaskStatus.IN_PROGRESS
|
||||
assert chunk.started_at is not None
|
||||
assert chunk.session_id == 1
|
||||
|
||||
def test_chunk_complete(self):
|
||||
"""Chunk can be completed."""
|
||||
chunk = Chunk(id="test", description="Test")
|
||||
"""Subtask can be completed."""
|
||||
chunk = Subtask(id="test", description="Test")
|
||||
chunk.start(session_id=1)
|
||||
|
||||
chunk.complete(output="Done successfully")
|
||||
|
||||
assert chunk.status == ChunkStatus.COMPLETED
|
||||
assert chunk.status == SubtaskStatus.COMPLETED
|
||||
assert chunk.completed_at is not None
|
||||
assert chunk.actual_output == "Done successfully"
|
||||
|
||||
def test_chunk_fail(self):
|
||||
"""Chunk can be marked as failed."""
|
||||
chunk = Chunk(id="test", description="Test")
|
||||
"""Subtask can be marked as failed."""
|
||||
chunk = Subtask(id="test", description="Test")
|
||||
chunk.start(session_id=1)
|
||||
|
||||
chunk.fail(reason="Test error")
|
||||
|
||||
assert chunk.status == ChunkStatus.FAILED
|
||||
assert chunk.status == SubtaskStatus.FAILED
|
||||
assert "FAILED: Test error" in chunk.actual_output
|
||||
|
||||
def test_chunk_to_dict(self):
|
||||
"""Chunk serializes to dict correctly."""
|
||||
chunk = Chunk(
|
||||
"""Subtask serializes to dict correctly."""
|
||||
chunk = Subtask(
|
||||
id="chunk-1",
|
||||
description="Test chunk",
|
||||
service="backend",
|
||||
@@ -113,7 +113,7 @@ class TestChunk:
|
||||
assert "file.py" in data["files_to_modify"]
|
||||
|
||||
def test_chunk_from_dict(self):
|
||||
"""Chunk deserializes from dict correctly."""
|
||||
"""Subtask deserializes from dict correctly."""
|
||||
data = {
|
||||
"id": "chunk-1",
|
||||
"description": "Test chunk",
|
||||
@@ -121,10 +121,10 @@ class TestChunk:
|
||||
"service": "frontend",
|
||||
}
|
||||
|
||||
chunk = Chunk.from_dict(data)
|
||||
chunk = Subtask.from_dict(data)
|
||||
|
||||
assert chunk.id == "chunk-1"
|
||||
assert chunk.status == ChunkStatus.COMPLETED
|
||||
assert chunk.status == SubtaskStatus.COMPLETED
|
||||
assert chunk.service == "frontend"
|
||||
|
||||
|
||||
@@ -184,8 +184,8 @@ class TestPhase:
|
||||
|
||||
def test_create_phase(self):
|
||||
"""Creates a phase with chunks."""
|
||||
chunk1 = Chunk(id="c1", description="Chunk 1")
|
||||
chunk2 = Chunk(id="c2", description="Chunk 2")
|
||||
chunk1 = Subtask(id="c1", description="Chunk 1")
|
||||
chunk2 = Subtask(id="c2", description="Chunk 2")
|
||||
|
||||
phase = Phase(
|
||||
phase=1,
|
||||
@@ -200,37 +200,37 @@ class TestPhase:
|
||||
|
||||
def test_phase_is_complete(self):
|
||||
"""Phase completion checks all chunks."""
|
||||
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
|
||||
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.COMPLETED)
|
||||
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
|
||||
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.COMPLETED)
|
||||
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
|
||||
|
||||
assert phase.is_complete() is True
|
||||
|
||||
def test_phase_not_complete_with_pending(self):
|
||||
"""Phase not complete with pending chunks."""
|
||||
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
|
||||
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.PENDING)
|
||||
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
|
||||
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.PENDING)
|
||||
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
|
||||
|
||||
assert phase.is_complete() is False
|
||||
|
||||
def test_phase_get_pending_chunks(self):
|
||||
"""Gets pending chunks from phase."""
|
||||
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
|
||||
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.PENDING)
|
||||
chunk3 = Chunk(id="c3", description="Chunk 3", status=ChunkStatus.PENDING)
|
||||
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
|
||||
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.PENDING)
|
||||
chunk3 = Subtask(id="c3", description="Chunk 3", status=SubtaskStatus.PENDING)
|
||||
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
|
||||
|
||||
pending = phase.get_pending_chunks()
|
||||
|
||||
assert len(pending) == 2
|
||||
assert all(c.status == ChunkStatus.PENDING for c in pending)
|
||||
assert all(c.status == SubtaskStatus.PENDING for c in pending)
|
||||
|
||||
def test_phase_get_progress(self):
|
||||
"""Gets progress counts from phase."""
|
||||
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
|
||||
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.COMPLETED)
|
||||
chunk3 = Chunk(id="c3", description="Chunk 3", status=ChunkStatus.PENDING)
|
||||
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
|
||||
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.COMPLETED)
|
||||
chunk3 = Subtask(id="c3", description="Chunk 3", status=SubtaskStatus.PENDING)
|
||||
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
|
||||
|
||||
completed, total = phase.get_progress()
|
||||
@@ -240,7 +240,7 @@ class TestPhase:
|
||||
|
||||
def test_phase_to_dict(self):
|
||||
"""Phase serializes to dict."""
|
||||
chunk = Chunk(id="c1", description="Test")
|
||||
chunk = Subtask(id="c1", description="Test")
|
||||
phase = Phase(
|
||||
phase=1,
|
||||
name="Setup",
|
||||
@@ -295,7 +295,7 @@ class TestImplementationPlan:
|
||||
|
||||
# Mark phase 1 as complete
|
||||
for chunk in plan.phases[0].subtasks:
|
||||
chunk.status = ChunkStatus.COMPLETED
|
||||
chunk.status = SubtaskStatus.COMPLETED
|
||||
|
||||
available = plan.get_available_phases()
|
||||
|
||||
@@ -314,14 +314,14 @@ class TestImplementationPlan:
|
||||
phase, subtask = result
|
||||
# Should be first pending subtask in phase 1
|
||||
assert phase.phase == 1
|
||||
assert subtask.status == ChunkStatus.PENDING
|
||||
assert subtask.status == SubtaskStatus.PENDING
|
||||
|
||||
def test_plan_get_progress(self, sample_implementation_plan: dict):
|
||||
"""Gets overall progress."""
|
||||
plan = ImplementationPlan.from_dict(sample_implementation_plan)
|
||||
|
||||
# Complete some subtasks
|
||||
plan.phases[0].subtasks[0].status = ChunkStatus.COMPLETED
|
||||
plan.phases[0].subtasks[0].status = SubtaskStatus.COMPLETED
|
||||
|
||||
progress = plan.get_progress()
|
||||
|
||||
@@ -449,7 +449,7 @@ class TestCreateInvestigationPlan:
|
||||
|
||||
# Fix phase should have blocked chunks
|
||||
fix_phase = plan.phases[2] # Phase 3 - Fix
|
||||
assert any(c.status == ChunkStatus.BLOCKED for c in fix_phase.subtasks)
|
||||
assert any(c.status == SubtaskStatus.BLOCKED for c in fix_phase.subtasks)
|
||||
|
||||
|
||||
class TestCreateRefactorPlan:
|
||||
@@ -500,10 +500,10 @@ class TestDependencyResolution:
|
||||
feature="Test",
|
||||
phases=[
|
||||
Phase(phase=1, name="Setup", subtasks=[
|
||||
Chunk(id="c1", description="Setup", status=ChunkStatus.PENDING)
|
||||
Subtask(id="c1", description="Setup", status=SubtaskStatus.PENDING)
|
||||
]),
|
||||
Phase(phase=2, name="Build", depends_on=[1], subtasks=[
|
||||
Chunk(id="c2", description="Build")
|
||||
Subtask(id="c2", description="Build")
|
||||
]),
|
||||
],
|
||||
)
|
||||
@@ -520,13 +520,13 @@ class TestDependencyResolution:
|
||||
feature="Test",
|
||||
phases=[
|
||||
Phase(phase=1, name="Setup", subtasks=[
|
||||
Chunk(id="c1", description="Setup", status=ChunkStatus.COMPLETED)
|
||||
Subtask(id="c1", description="Setup", status=SubtaskStatus.COMPLETED)
|
||||
]),
|
||||
Phase(phase=2, name="Backend", depends_on=[1], subtasks=[
|
||||
Chunk(id="c2", description="Backend")
|
||||
Subtask(id="c2", description="Backend")
|
||||
]),
|
||||
Phase(phase=3, name="Frontend", depends_on=[1], subtasks=[
|
||||
Chunk(id="c3", description="Frontend")
|
||||
Subtask(id="c3", description="Frontend")
|
||||
]),
|
||||
],
|
||||
)
|
||||
@@ -545,13 +545,13 @@ class TestDependencyResolution:
|
||||
feature="Test",
|
||||
phases=[
|
||||
Phase(phase=1, name="Phase1", subtasks=[
|
||||
Chunk(id="c1", description="C1", status=ChunkStatus.COMPLETED)
|
||||
Subtask(id="c1", description="C1", status=SubtaskStatus.COMPLETED)
|
||||
]),
|
||||
Phase(phase=2, name="Phase2", subtasks=[
|
||||
Chunk(id="c2", description="C2", status=ChunkStatus.PENDING)
|
||||
Subtask(id="c2", description="C2", status=SubtaskStatus.PENDING)
|
||||
]),
|
||||
Phase(phase=3, name="Phase3", depends_on=[1, 2], subtasks=[
|
||||
Chunk(id="c3", description="C3")
|
||||
Subtask(id="c3", description="C3")
|
||||
]),
|
||||
],
|
||||
)
|
||||
@@ -563,12 +563,12 @@ class TestDependencyResolution:
|
||||
assert 3 not in phase_nums
|
||||
|
||||
|
||||
class TestChunkCritique:
|
||||
"""Tests for self-critique functionality on chunks."""
|
||||
class TestSubtaskCritique:
|
||||
"""Tests for self-critique functionality on subtasks."""
|
||||
|
||||
def test_chunk_stores_critique_result(self):
|
||||
"""Chunk can store critique results."""
|
||||
chunk = Chunk(id="test", description="Test")
|
||||
"""Subtask can store critique results."""
|
||||
chunk = Subtask(id="test", description="Test")
|
||||
|
||||
chunk.critique_result = {
|
||||
"passed": True,
|
||||
@@ -580,7 +580,7 @@ class TestChunkCritique:
|
||||
|
||||
def test_critique_serializes(self):
|
||||
"""Critique result serializes correctly."""
|
||||
chunk = Chunk(id="test", description="Test")
|
||||
chunk = Subtask(id="test", description="Test")
|
||||
chunk.critique_result = {"passed": False, "issues": ["Missing tests"]}
|
||||
|
||||
data = chunk.to_dict()
|
||||
@@ -596,7 +596,7 @@ class TestChunkCritique:
|
||||
"critique_result": {"passed": True, "score": 8},
|
||||
}
|
||||
|
||||
chunk = Chunk.from_dict(data)
|
||||
chunk = Subtask.from_dict(data)
|
||||
|
||||
assert chunk.critique_result is not None
|
||||
assert chunk.critique_result["score"] == 8
|
||||
|
||||
@@ -23,7 +23,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from merge import (
|
||||
ChangeType,
|
||||
|
||||
@@ -20,7 +20,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from merge import (
|
||||
ChangeType,
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
# Add tests directory to path for test_fixtures
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Callable, Generator
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from merge import (
|
||||
SemanticAnalyzer,
|
||||
|
||||
@@ -23,7 +23,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
# Add tests directory to path for test_fixtures
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from workspace import ParallelMergeTask, ParallelMergeResult
|
||||
from core.workspace import _run_parallel_merges
|
||||
|
||||
@@ -19,7 +19,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
# Add tests directory to path for test_fixtures
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from merge import (
|
||||
ChangeType,
|
||||
|
||||
@@ -100,7 +100,7 @@ mock_client.create_client = MagicMock()
|
||||
sys.modules['client'] = mock_client
|
||||
|
||||
# Now we can safely add the auto-claude path and import
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
# Import criteria functions directly to avoid going through qa/__init__.py
|
||||
# which imports reviewer and fixer that need the SDK
|
||||
|
||||
@@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from qa_loop import (
|
||||
# Iteration tracking
|
||||
|
||||
@@ -17,7 +17,7 @@ from pathlib import Path
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from risk_classifier import (
|
||||
RiskClassifier,
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from security_scanner import (
|
||||
SecurityVulnerability,
|
||||
|
||||
@@ -17,9 +17,9 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from service_orchestrator import (
|
||||
from services.orchestrator import (
|
||||
ServiceConfig,
|
||||
OrchestrationResult,
|
||||
ServiceOrchestrator,
|
||||
|
||||
@@ -50,7 +50,7 @@ sys.modules['claude_agent_sdk'] = mock_agent_sdk
|
||||
sys.modules['claude_agent_sdk.types'] = mock_agent_types
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from spec.complexity import (
|
||||
Complexity,
|
||||
|
||||
@@ -18,7 +18,7 @@ from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Add auto-claude directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
# Store original modules for cleanup
|
||||
_original_modules = {}
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add auto-claude to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from phase_config import THINKING_BUDGET_MAP, get_thinking_budget
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import pytest
|
||||
|
||||
# Add auto-claude to path for imports
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "Apps" / "backend"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
||||
|
||||
from validation_strategy import (
|
||||
from spec.validation_strategy import (
|
||||
ValidationStep,
|
||||
ValidationStrategy,
|
||||
ValidationStrategyBuilder,
|
||||
|
||||
Reference in New Issue
Block a user