Skip to content

Commit f330c5d

Browse files
MohabMohieclaude
andauthored
shaft-intellij: unwrap capture_status/capture_api_status union before reading fields (#3954)
GuidedWorkflowPanel's background status poller (applyStatusPoll and its helpers) read state/active/eventCount/steps directly off the top-level capture_status/capture_api_status JSON. Since #3881, CaptureService#status()/ apiStatus() return a union (McpCaptureUnionStatus/McpCaptureApiUnionStatus) with the real status nested under webStatus/playwrightStatus/mobileStatus -- GuidedWorkflowLiveE2ETest's webStatus()/mobileStatus() helpers already proved this wire shape against a live server. The poller never unwrapped it, so an active recording never rendered as active in the Guided workflow status strip for any backend. Fix: unwrap the union to its populated engine section once, right after parsing the poll response, before any field is read (mirrors the E2E test's unwrap helpers). Added a regression test that feeds applyStatusPoll a real union-shaped fixture and asserts on the rendered status text. Closes #3949 Claude-Session: https://claude.ai/code/session_01NDQpYzvnTbDPYwqDwHQoRz Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ec760e0 commit f330c5d

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

shaft-intellij/src/main/java/com/shaft/intellij/ui/GuidedWorkflowPanel.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ final class GuidedWorkflowPanel extends JPanel implements Disposable {
9494
private final String recordingKey = "guided#" + Integer.toHexString(System.identityHashCode(this));
9595
// Created lazily on the first poll so headless panel tests never touch platform executors.
9696
private Alarm statusPollAlarm;
97-
private volatile boolean pollingActive;
97+
// Package-private (not private) for GuidedWorkflowPanelTest, matching the applyStatusPoll()
98+
// test seam below: exercises the real poll-response handling path against a status fixture
99+
// without needing a live polling MCP connection to flip this on via startStatusPolling().
100+
volatile boolean pollingActive;
98101
private boolean headlessLockedByPolicy;
99102
private boolean recorderSeenActive;
100103
private int pollsWithoutActivity;
@@ -1210,7 +1213,10 @@ private void pollStatusOnce() {
12101213
.whenComplete((result, error) -> onEdt(() -> applyStatusPoll(result, error)));
12111214
}
12121215

1213-
private void applyStatusPoll(ShaftMcpToolResult result, Throwable error) {
1216+
// Package-private (not private) for GuidedWorkflowPanelTest (mirrors renderStepsFromStatus()
1217+
// below): exercises the real poll-response handling path against a status fixture directly,
1218+
// without needing a live polling MCP connection.
1219+
void applyStatusPoll(ShaftMcpToolResult result, Throwable error) {
12141220
if (!pollingActive) {
12151221
return;
12161222
}
@@ -1219,7 +1225,7 @@ private void applyStatusPoll(ShaftMcpToolResult result, Throwable error) {
12191225
scheduleNextStatusPoll();
12201226
return;
12211227
}
1222-
JsonObject status = AssistantMarkdown.jsonObjectFromMcpOutput(result.output());
1228+
JsonObject status = unwrapCaptureStatus(AssistantMarkdown.jsonObjectFromMcpOutput(result.output()));
12231229
// Issue #3639: the steps list is a pure projection of this same polled status, refreshed on
12241230
// every poll (active or the final post-stop poll) -- there is no separate client-side state
12251231
// store. A capture_status payload (WebDriver) has no "steps" key, so this naturally renders
@@ -1398,6 +1404,34 @@ public String toString() {
13981404
}
13991405
}
14001406

1407+
/**
1408+
* Unwraps a {@code capture_status}/{@code capture_api_status} union response ({@code
1409+
* McpCaptureUnionStatus}/{@code McpCaptureApiUnionStatus}, design doc amendment A3, landed by
1410+
* #3881) down to whichever of {@code webStatus}/{@code playwrightStatus}/{@code mobileStatus}
1411+
* the active engine populated -- mirrors {@code GuidedWorkflowLiveE2ETest}'s {@code
1412+
* webStatus()}/{@code mobileStatus()} helpers, which confirmed this wire shape against a live
1413+
* server (issue #3949): {@code state}/{@code active}/{@code actionCount}/{@code steps} live on
1414+
* that nested section, not at the union's top level. The union guarantees exactly one section is
1415+
* populated, so returning the first populated section is equivalent to dispatching on the
1416+
* union's {@code engine} field. A payload with none of these keys (already unwrapped, or a
1417+
* non-union shape) is returned unchanged.
1418+
*
1419+
* @param status the raw parsed tool output, or null
1420+
* @return the unwrapped engine-specific status section, or {@code status} itself when it has no
1421+
* union section to unwrap
1422+
*/
1423+
private static JsonObject unwrapCaptureStatus(JsonObject status) {
1424+
if (status == null) {
1425+
return null;
1426+
}
1427+
for (String section : new String[]{"webStatus", "playwrightStatus", "mobileStatus"}) {
1428+
if (status.has(section) && status.get(section).isJsonObject()) {
1429+
return status.getAsJsonObject(section);
1430+
}
1431+
}
1432+
return status;
1433+
}
1434+
14011435
private static boolean isRecorderActive(JsonObject status) {
14021436
if (status == null) {
14031437
return false;

shaft-intellij/src/test/java/com/shaft/intellij/ui/GuidedWorkflowPanelTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.shaft.intellij.ui;
22

33
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonNull;
45
import com.google.gson.JsonObject;
56
import com.intellij.openapi.project.Project;
7+
import com.shaft.intellij.mcp.ShaftMcpToolResult;
68
import com.intellij.ui.components.JBList;
79
import org.junit.jupiter.api.Test;
810

@@ -483,6 +485,44 @@ void parseStepsReadsPerStepFieldsAndTreatsMissingStepsAsEmpty() {
483485
assertTrue(GuidedWorkflowPanel.parseSteps(null).isEmpty());
484486
}
485487

488+
@Test
489+
void applyStatusPollUnwrapsTheActiveEngineSectionFromTheUnionResponse() {
490+
// Issue #3949: capture_status/capture_api_status return a union (McpCaptureUnionStatus /
491+
// McpCaptureApiUnionStatus, design doc amendment A3, landed by #3881) with the real recorder
492+
// status nested under webStatus/playwrightStatus/mobileStatus -- GuidedWorkflowLiveE2ETest's
493+
// webStatus()/mobileStatus() helpers (confirmed against a live server) prove this is the
494+
// actual wire shape, not the flat pre-#3881 shape. applyStatusPoll must unwrap the matching
495+
// section before reading state/eventCount, or an active recording never renders as active.
496+
GuidedWorkflowPanel panel = new GuidedWorkflowPanel(null, (tool, arguments) -> {
497+
});
498+
panel.pollingActive = true;
499+
500+
JsonObject webStatus = new JsonObject();
501+
webStatus.addProperty("state", "ACTIVE");
502+
webStatus.addProperty("eventCount", 2);
503+
webStatus.addProperty("readiness", "READY");
504+
webStatus.addProperty("currentUrl", "https://example.com/cart");
505+
506+
JsonObject union = new JsonObject();
507+
union.addProperty("engine", "WEB");
508+
union.add("webStatus", webStatus);
509+
union.add("playwrightStatus", JsonNull.INSTANCE);
510+
union.add("mobileStatus", JsonNull.INSTANCE);
511+
512+
try {
513+
panel.applyStatusPoll(new ShaftMcpToolResult(true, union.toString(), null, null), null);
514+
515+
String statusText = panel.recorderStatusLabel().getText();
516+
assertAll(
517+
() -> assertTrue(statusText.contains("Recording"),
518+
"Expected the WEB engine's nested webStatus to render as active: " + statusText),
519+
() -> assertTrue(statusText.contains("2 steps"),
520+
"Expected the unwrapped webStatus eventCount to render: " + statusText));
521+
} finally {
522+
panel.dispose();
523+
}
524+
}
525+
486526
private static JsonObject recordedStepsStatus(String stepId) {
487527
JsonObject status = new JsonObject();
488528
status.addProperty("active", true);

0 commit comments

Comments
 (0)