mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
feat: decouple regression testing agents from coding agents
Major refactoring of the parallel orchestrator to run regression testing agents independently from coding agents. This improves system reliability and provides better control over testing behavior. Key changes: Database & MCP Layer: - Add testing_in_progress and last_tested_at columns to Feature model - Add feature_claim_for_testing() for atomic test claim with retry - Add feature_release_testing() to release claims after testing - Refactor claim functions to iterative loops (no recursion) - Add OperationalError retry handling for transient DB errors - Reduce MAX_CLAIM_RETRIES from 10 to 5 Orchestrator: - Decouple testing agent lifecycle from coding agents - Add _maintain_testing_agents() for continuous testing maintenance - Fix TOCTOU race in _spawn_testing_agent() - hold lock during spawn - Add _cleanup_stale_testing_locks() with 30-min timeout - Fix log ordering - start_session() before stale flag cleanup - Add stale testing_in_progress cleanup on startup Dead Code Removal: - Remove count_testing_in_concurrency from entire stack (12+ files) - Remove ineffective with_for_update() from features router API & UI: - Pass testing_agent_ratio via CLI to orchestrator - Update testing prompt template to use new claim/release tools - Rename UI label to "Regression Agents" with clearer description - Add process_utils.py for cross-platform process tree management Testing agents now: - Run continuously as long as passing features exist - Can re-test features multiple times to catch regressions - Are controlled by fixed count (0-3) via testing_agent_ratio setting - Have atomic claiming to prevent concurrent testing of same feature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,6 @@ export function AgentControl({ projectName, status }: AgentControlProps) {
|
||||
parallelMode: isParallel,
|
||||
maxConcurrency: concurrency, // Always pass concurrency (1-5)
|
||||
testingAgentRatio: settings?.testing_agent_ratio,
|
||||
countTestingInConcurrency: settings?.count_testing_in_concurrency,
|
||||
})
|
||||
const handleStop = () => stopAgent.mutate()
|
||||
|
||||
|
||||
@@ -76,12 +76,6 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCountTestingToggle = () => {
|
||||
if (settings && !updateSettings.isPending) {
|
||||
updateSettings.mutate({ count_testing_in_concurrency: !settings.count_testing_in_concurrency })
|
||||
}
|
||||
}
|
||||
|
||||
const models = modelsData?.models ?? []
|
||||
const isSaving = updateSettings.isPending
|
||||
|
||||
@@ -211,16 +205,16 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testing Agent Ratio */}
|
||||
{/* Regression Agents */}
|
||||
<div>
|
||||
<label
|
||||
id="testing-ratio-label"
|
||||
className="font-display font-bold text-base block mb-1"
|
||||
>
|
||||
Testing Agents per Coding Agent
|
||||
Regression Agents
|
||||
</label>
|
||||
<p className="text-sm text-[var(--color-neo-text-secondary)] mb-2">
|
||||
Regression testing agents spawned per coding agent (0 = disabled)
|
||||
Number of regression testing agents (0 = disabled)
|
||||
</p>
|
||||
<div
|
||||
className="flex border-3 border-[var(--color-neo-border)]"
|
||||
@@ -246,41 +240,6 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Count Testing in Concurrency Toggle */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label
|
||||
id="count-testing-label"
|
||||
className="font-display font-bold text-base"
|
||||
>
|
||||
Count Testing in Concurrency
|
||||
</label>
|
||||
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
|
||||
If enabled, testing agents count toward the concurrency limit
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCountTestingToggle}
|
||||
disabled={isSaving}
|
||||
className={`relative w-14 h-8 rounded-none border-3 border-[var(--color-neo-border)] transition-colors ${
|
||||
settings.count_testing_in_concurrency
|
||||
? 'bg-[var(--color-neo-progress)]'
|
||||
: 'bg-[var(--color-neo-card)]'
|
||||
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
role="switch"
|
||||
aria-checked={settings.count_testing_in_concurrency}
|
||||
aria-labelledby="count-testing-label"
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 w-5 h-5 bg-[var(--color-neo-border)] transition-transform ${
|
||||
settings.count_testing_in_concurrency ? 'left-7' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Update Error */}
|
||||
{updateSettings.isError && (
|
||||
<div className="p-3 bg-[var(--color-neo-error-bg)] border-3 border-[var(--color-neo-error-border)] text-[var(--color-neo-error-text)] text-sm">
|
||||
|
||||
@@ -128,7 +128,6 @@ export function useStartAgent(projectName: string) {
|
||||
parallelMode?: boolean
|
||||
maxConcurrency?: number
|
||||
testingAgentRatio?: number
|
||||
countTestingInConcurrency?: boolean
|
||||
} = {}) => api.startAgent(projectName, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
|
||||
@@ -239,7 +238,6 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
glm_mode: false,
|
||||
testing_agent_ratio: 1,
|
||||
count_testing_in_concurrency: false,
|
||||
}
|
||||
|
||||
export function useAvailableModels() {
|
||||
|
||||
@@ -112,8 +112,8 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
agentStatus: message.status,
|
||||
// Clear active agents when process stops to prevent stale UI
|
||||
...(message.status === 'stopped' && { activeAgents: [], recentActivity: [] }),
|
||||
// Clear active agents when process stops OR crashes to prevent stale UI
|
||||
...((message.status === 'stopped' || message.status === 'crashed') && { activeAgents: [], recentActivity: [] }),
|
||||
}))
|
||||
break
|
||||
|
||||
|
||||
@@ -211,7 +211,6 @@ export async function startAgent(
|
||||
parallelMode?: boolean
|
||||
maxConcurrency?: number
|
||||
testingAgentRatio?: number
|
||||
countTestingInConcurrency?: boolean
|
||||
} = {}
|
||||
): Promise<AgentActionResponse> {
|
||||
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
|
||||
@@ -221,7 +220,6 @@ export async function startAgent(
|
||||
parallel_mode: options.parallelMode ?? false,
|
||||
max_concurrency: options.maxConcurrency,
|
||||
testing_agent_ratio: options.testingAgentRatio,
|
||||
count_testing_in_concurrency: options.countTestingInConcurrency,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,8 +129,7 @@ export interface AgentStatusResponse {
|
||||
model: string | null // Model being used by running agent
|
||||
parallel_mode: boolean // DEPRECATED: Always true now (unified orchestrator)
|
||||
max_concurrency: number | null
|
||||
testing_agent_ratio: number // Testing agents per coding agent (0-3)
|
||||
count_testing_in_concurrency: boolean // Count testing toward concurrency limit
|
||||
testing_agent_ratio: number // Regression testing agents (0-3)
|
||||
}
|
||||
|
||||
export interface AgentActionResponse {
|
||||
@@ -479,15 +478,13 @@ export interface Settings {
|
||||
yolo_mode: boolean
|
||||
model: string
|
||||
glm_mode: boolean
|
||||
testing_agent_ratio: number // Testing agents per coding agent (0-3)
|
||||
count_testing_in_concurrency: boolean // Count testing toward concurrency limit
|
||||
testing_agent_ratio: number // Regression testing agents (0-3)
|
||||
}
|
||||
|
||||
export interface SettingsUpdate {
|
||||
yolo_mode?: boolean
|
||||
model?: string
|
||||
testing_agent_ratio?: number
|
||||
count_testing_in_concurrency?: boolean
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user