diff --git a/agentic_harness/core/verifier_manifest.py b/agentic_harness/core/verifier_manifest.py index f5b0b9d..7b901be 100644 --- a/agentic_harness/core/verifier_manifest.py +++ b/agentic_harness/core/verifier_manifest.py @@ -413,10 +413,10 @@ def _add_globs(root: Path, candidates: set[Path], patterns: tuple[str, ...]) -> (?: \btest | - \b(?:getByName|named)(?:<[^>]+>)?\(\s*["']test["']\s*\)(?:\.\s*configure)? + \b(?:getByName|named)(?:<[^>]+>)?\(\s*["']test["']\s*\) ) (?:\s*\.\s*get\s*\(\s*\))? - (?:\s*\.\s*(?:configure|apply))? + (?:\.\s*(?P[A-Za-z_][A-Za-z0-9_]*)\s*)? \s*\{ """ ) @@ -577,6 +577,7 @@ def _gradle_test_roots(root: Path, *, allow_dynamic: bool) -> set[Path]: expressions: list[str] = [] unresolved_source_root = False for match in _GRADLE_TEST_BLOCK.finditer(text): + method = match.group("method") block = _balanced_brace_body(text, match.end() - 1) source_matches = list(_GRADLE_SOURCE_ROOT_CALL.finditer(block)) expressions.extend( @@ -586,6 +587,9 @@ def _gradle_test_roots(root: Path, *, allow_dynamic: bool) -> set[Path]: unresolved_source_root = unresolved_source_root or ( len(_GRADLE_SOURCE_ROOT_NAME.findall(block)) != len(source_matches) ) + unresolved_source_root = unresolved_source_root or ( + method is not None and method not in {"configure", "apply"} + ) for item in _GRADLE_DIRECT_TEST_CONFIG.finditer(text): direct_config = item.group(0) source_matches = list(_GRADLE_SOURCE_ROOT_CALL.finditer(direct_config)) diff --git a/agentic_harness/gui/static/app.js b/agentic_harness/gui/static/app.js index d46909d..5e13261 100644 --- a/agentic_harness/gui/static/app.js +++ b/agentic_harness/gui/static/app.js @@ -1166,11 +1166,19 @@ function renderRecovery() { const task = state.liveTask || state.currentTask || {}; const blockingForegroundReview = isBlockingForegroundReview(task); const visibleState = blockingForegroundReview ? "needs_review" : readinessState; + const setupStates = [ + "setup_required", + "credential_required", + "verification_required", + "connection_test_required", + ]; + const needsSetup = setupStates.includes(readinessState); const canContinue = hasAction(task, "continue"); const canStop = hasAction(task, "stop"); const hasTask = Boolean(task?.id); const show = blockingForegroundReview || state.readiness?.can_start === false - && ["needs_review", "needs_attention", "blocked", "configuration_error"].includes(readinessState); + && ["needs_review", "needs_attention", "blocked", "configuration_error", ...setupStates].includes(readinessState); + const canOpenSettings = needsSetup || readinessState === "configuration_error"; els.recoveryCard.hidden = !show; if (!show) { @@ -1186,6 +1194,8 @@ function renderRecovery() { ? guide.title || "Your result is ready" : visibleState === "configuration_error" ? "Configuration needs repair" + : needsSetup + ? "Setup needed before continuing" : "Current task needs attention"; els.recoverySummary.textContent = visibleState === "needs_review" ? guide.explanation || "The assistant finished and stopped safely for your review." @@ -1200,21 +1210,42 @@ function renderRecovery() { : "Stop task"; els.recoveryOpenTaskButton.textContent = visibleState === "needs_review" ? "Review result" - : "Open current task"; + : canOpenSettings + ? "Open settings" + : "Open current task"; els.recoveryContinueButton.hidden = !hasTask || !canContinue; els.recoveryStopButton.hidden = !hasTask || !canStop; - els.recoveryOpenTaskButton.hidden = !hasTask; + els.recoveryOpenTaskButton.hidden = !hasTask && !canOpenSettings; els.recoveryStatus.textContent = hasTask ? canContinue || canStop ? "Use a button here, or open the task to review its details and result." - : "Open the current task to see the exact blocker and available decision." - : "Refresh once. If this remains blocked, the installation owner must repair the configuration."; + : canOpenSettings + ? "Open Settings to resolve this setup step, then return to this screen." + : "Open the current task to see the exact blocker and available decision." + : canOpenSettings + ? "Open Settings, complete this setup step, then come back." + : "Refresh once. If this remains blocked, the installation owner must repair the configuration."; } function openCurrentTaskFromRecovery() { + const readinessState = String(state.readiness?.state || ""); + if ( + [ + "setup_required", + "credential_required", + "verification_required", + "connection_test_required", + "configuration_error", + ].includes(readinessState) + ) { + showView("settings", { focus: true }); + return; + } if (state.liveTask) { state.viewingHistoryId = ""; renderTask(state.liveTask); + showView("tasks", { focus: true }); + return; } showView("tasks", { focus: true }); } @@ -2022,9 +2053,11 @@ function updateDemoCallout(task = null) { } els.demoButton.disabled = state.busy || active; els.setupDemoButton.disabled = state.busy || active; - els.demoSetupButton.hidden = managedOverlay ? !isDemo || active : false; + els.demoSetupButton.hidden = active; els.demoSetupButton.textContent = managedOverlay - ? "Return to real workspace" + ? isDemo + ? "Return to real workspace" + : "Open settings" : "Connect real work"; els.setupDemoButton.textContent = verified ? "Run safe demo again" diff --git a/tests/frontend_token_race_test.js b/tests/frontend_token_race_test.js index 8b26220..b359b16 100644 --- a/tests/frontend_token_race_test.js +++ b/tests/frontend_token_race_test.js @@ -1250,6 +1250,42 @@ async function testManagedExpectationStaysPlainAndMobileGetsUnavailableReasons() assert.equal(newRouteCard.focusCount, 1); } +async function testSetupRequiredStateShowsRecoverySettingsShortcut() { + const app = await runApp({ + publicAccess: true, + setupPayload: { + contract: "agentic_harness.gui_setup.v1", + configured: false, + editable: true, + workspace: "/tmp/new-project", + suggested_check: "npm test", + }, + healthPayload: { + ok: true, + readiness: { + state: "setup_required", + can_start: false, + summary: "Choose an AI connection.", + next_action: "Open Settings and connect an assistant before starting real work.", + }, + }, + taskPayload: { + status: "ready", + status_label: "Ready", + result_category: "in_progress", + summary: "Backend ready", + progress: { determinate: false, percent: null, label: "" }, + }, + }); + + assert.equal(app.elements.get("recoveryCard").hidden, false); + assert.equal(app.elements.get("recoveryOpenTaskButton").hidden, false); + assert.equal(app.elements.get("recoveryOpenTaskButton").textContent, "Open settings"); + app.elements.get("recoveryOpenTaskButton").listeners.click(); + await tick(); + assert.equal(app.elements.get("settingsView").hidden, false); +} + async function testResetNeverSilentlyAppliesACloudDefaultRoute() { const setupPayload = { contract: "agentic_harness.gui_setup.v1", @@ -1921,6 +1957,7 @@ async function testLostStartResponseReconnectsToTheAcceptedTask() { await testLegacyHumanCanChooseEveryModeWithoutWritingACommand(); await testPredictableViewsConciseModesAndAccessSummaryKeepDraftState(); await testExpectationReflectsSetupReadinessInsteadOfClaimingReady(); + await testSetupRequiredStateShowsRecoverySettingsShortcut(); await testExpectationUsesPlainDynamicFactsAndCardSelectionKeepsFocus(); await testManagedExpectationStaysPlainAndMobileGetsUnavailableReasons(); await testResetNeverSilentlyAppliesACloudDefaultRoute(); diff --git a/tests/test_verified_tournament.py b/tests/test_verified_tournament.py index 82e89bc..dc8a7c4 100644 --- a/tests/test_verified_tournament.py +++ b/tests/test_verified_tournament.py @@ -857,6 +857,29 @@ def test_unrecognized_gradle_test_alias_closure_fails_closed(tmp_path: Path) -> tournament_module._freeze_verifier_assets(root, [["gradle", "test"]]) +def test_sourceSets_named_with_convention_fails_closed(tmp_path: Path) -> None: + root, _ = _project(tmp_path) + (root / "verification").mkdir() + (root / "build.gradle.kts").write_text( + ( + 'plugins { java }\n' + 'sourceSets.named("test").withConvention {\n' + ' java.srcDir("verification")\n' + "}\n" + ), + encoding="utf-8", + ) + (root / "verification" / "BoundaryTest.java").write_text( + "class BoundaryTest {}\n", + encoding="utf-8", + ) + _git(root, "add", ".") + _git(root, "commit", "-m", "add unsupported direct sourceSets closure") + + with pytest.raises(ConfigError, match="cannot infer a Gradle test source root syntax"): + tournament_module._freeze_verifier_assets(root, [["gradle", "test"]]) + + @pytest.mark.parametrize( "alias_expression", [