Skip to content

Commit 27d8cad

Browse files
fix: loop node GraphQL runner extraction and execution status hardening (#524)
Co-authored-by: Wei Lin <wei@avaprotocol.org>
1 parent edf1621 commit 27d8cad

3 files changed

Lines changed: 233 additions & 8 deletions

File tree

core/taskengine/vm.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4188,6 +4188,10 @@ func (eq *ExecutionQueue) executeTask(task *ExecutionTask) *ExecutionResult {
41884188
nodeType = "customCode"
41894189
case step.GetRestApi() != nil:
41904190
nodeType = "restApi"
4191+
case step.GetGraphql() != nil:
4192+
nodeType = "graphql"
4193+
case step.GetEthTransfer() != nil:
4194+
nodeType = "ethTransfer"
41914195
}
41924196
eq.vm.logger.Info("Loop iteration result extracted",
41934197
"step_id", task.StepID,
@@ -4253,6 +4257,9 @@ func (eq *ExecutionQueue) extractResultData(step *avsproto.Execution_Step) inter
42534257
txHash := extractTxHashFromFlatMetadata(step.Metadata)
42544258
return wrapResultWithTxHash(resultData, txHash)
42554259
}
4260+
if graphqlOutput := step.GetGraphql(); graphqlOutput != nil && graphqlOutput.Data != nil {
4261+
return graphqlOutput.Data.AsInterface()
4262+
}
42564263
return nil
42574264
}
42584265

core/taskengine/vm_runner_graphql_query_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,89 @@ func TestGraphlQlNodeSimpleQuery(t *testing.T) {
171171
t.Errorf("decimals doesn't match. expected %d got %d", 6, output.Markets[1].InputToken.Decimals)
172172
}
173173
}
174+
175+
// Test that Loop node with GraphQL runner correctly extracts iteration results.
176+
// This is a regression test for https://github.com/AvaProtocol/EigenLayer-AVS/issues/523
177+
// where extractResultData was missing the GraphQL output case, causing all GraphQL
178+
// loop iterations to be counted as "failed" even though the HTTP requests succeeded.
179+
func TestLoopWithGraphQLRunner(t *testing.T) {
180+
requestCount := 0
181+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
182+
requestCount++
183+
w.Header().Set("Content-Type", "application/json")
184+
fmt.Fprint(w, `{
185+
"data": {
186+
"country": {
187+
"name": "TestCountry",
188+
"code": "TC"
189+
}
190+
}
191+
}`)
192+
}))
193+
defer mockServer.Close()
194+
195+
// Build a loop node with GraphQL runner
196+
loopConfig := map[string]interface{}{
197+
"inputVariable": "{{countryCodes}}",
198+
"iterVal": "value",
199+
"iterKey": "index",
200+
"runner": map[string]interface{}{
201+
"type": "graphqlDataQuery",
202+
"config": map[string]interface{}{
203+
"url": mockServer.URL,
204+
"query": `query { country(code: "{{value}}") { name code } }`,
205+
},
206+
},
207+
}
208+
209+
node, err := CreateNodeFromType(NodeTypeLoop, loopConfig, "loop_graphql_test")
210+
if err != nil {
211+
t.Fatalf("failed to create loop node: %v", err)
212+
}
213+
214+
inputVariables := map[string]interface{}{
215+
"countryCodes": []interface{}{"US", "JP"},
216+
}
217+
218+
vm, err := NewVMWithData(nil, nil, testutil.GetTestSmartWalletConfig(), nil)
219+
if err != nil {
220+
t.Fatalf("failed to create VM: %v", err)
221+
}
222+
vm.WithLogger(testutil.GetLogger())
223+
224+
step, err := vm.RunNodeWithInputs(node, inputVariables)
225+
if err != nil {
226+
t.Fatalf("expected loop to succeed but got error: %v", err)
227+
}
228+
229+
if !step.Success {
230+
t.Fatalf("expected loop step to succeed but got failure: %s", step.Error)
231+
}
232+
233+
// Verify GraphQL HTTP requests were actually made
234+
if requestCount != 2 {
235+
t.Errorf("expected 2 GraphQL HTTP requests but got %d", requestCount)
236+
}
237+
238+
// Verify loop output data contains results from both iterations
239+
loopOutput := step.GetLoop()
240+
if loopOutput == nil || loopOutput.Data == nil {
241+
t.Fatal("expected loop output data but got nil")
242+
}
243+
244+
outputArray, ok := loopOutput.Data.AsInterface().([]interface{})
245+
if !ok {
246+
t.Fatalf("expected loop output to be an array, got %T", loopOutput.Data.AsInterface())
247+
}
248+
249+
if len(outputArray) != 2 {
250+
t.Fatalf("expected 2 results but got %d", len(outputArray))
251+
}
252+
253+
// Verify each iteration result is non-nil (the fix for issue #523)
254+
for i, result := range outputArray {
255+
if result == nil {
256+
t.Errorf("iteration %d result is nil - extractResultData is not handling GraphQL output", i)
257+
}
258+
}
259+
}

docs/changes/0001-execution-status-redesign.md

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,145 @@ enum ExecutionStatus {
7272
- Enum value `4` is reserved and will not be reused.
7373
- New enum value `ERROR = 5` for system-level failures.
7474

75-
## SDK/Client Migration
75+
## How the Client Should Interpret Execution Status
7676

77-
1. Remove any `PartialSuccess` handling or `isConditionalSkip` workarounds.
78-
2. Treat `SUCCESS` as the only positive outcome. Branch skips no longer
79-
produce a warning status.
80-
3. Treat `FAILED` as the single status for any node-level execution failure,
81-
regardless of whether some or all steps failed.
82-
4. Treat `ERROR` as a system-level problem (not caused by the workflow
83-
configuration itself).
77+
### Current state (SDK still has `PartialSuccess`)
78+
79+
The SDK (`@avaprotocol/types`) still exposes four runtime statuses:
80+
`Success`, `PartialSuccess`, `Failed`, `Pending`. The backend returns
81+
`partialSuccess` for **both** conditional branch skips and actual step
82+
failures, so the client must disambiguate.
83+
84+
**Decision tree for `PartialSuccess`:**
85+
86+
```
87+
status === PartialSuccess
88+
└─ steps.every(s => s.success) ?
89+
├─ YES → conditional skip (treat as success)
90+
└─ NO → real step failure (treat as error/warning)
91+
```
92+
93+
**Full client interpretation table (current SDK):**
94+
95+
| `status` | `steps.every(s => s.success)` | Interpretation | UI treatment |
96+
|------------------|-------------------------------|------------------------|----------------------------------------------|
97+
| `Success` | true | All nodes ran, all OK | Green — "Simulation completed" |
98+
| `PartialSuccess` | true | Branch skipped nodes | Green — "Simulation completed — Some steps skipped by condition" |
99+
| `PartialSuccess` | false | Real step failure(s) | Yellow — "Simulation completed with errors" |
100+
| `Failed` | false | All-or-nothing failure | Red — "Simulation completed with errors" |
101+
| `Pending` | n/a | Still running | Neutral — loading state |
102+
103+
**Where this logic lives in Studio today:**
104+
105+
1. **Simulation status label** (`components/CanvasToolbarBottom/index.tsx`):
106+
```ts
107+
const isSuccess = simulationResult?.status === ExecutionStatus.Success;
108+
const isFailed = simulationResult?.status === ExecutionStatus.Failed;
109+
const isConditionalSkip =
110+
simulationResult?.status === ExecutionStatus.PartialSuccess &&
111+
simulationResult?.steps?.every((step: StepProps) => step.success);
112+
const hasStepErrors = !isSuccess && !isConditionalSkip;
113+
```
114+
- `isConditionalSkip` → green label, normal "Deploy" button
115+
- `hasStepErrors` → yellow/red label, "Deploy Anyway" button with caution tooltip
116+
117+
2. **Simulation gate for deploy** (`app/types/simulation.ts`):
118+
Only `ExecutionStatus.Success` passes the gate. `PartialSuccess` (even
119+
conditional skips) currently fails the gate — this needs updating after
120+
the SDK change so that the gate also passes for `Success` with skipped
121+
steps.
122+
123+
3. **Execution time display** (`CanvasToolbarBottom getExecutionTimeDisplay`):
124+
Maps status to text color: `Success` → green, `PartialSuccess` → yellow,
125+
`Failed` → red, `Pending` → yellow.
126+
127+
4. **Execution history chips** (`components/workflows/StatusChips.tsx`):
128+
- `Success` → "Completed" (green check)
129+
- `PartialSuccess` → "Partial Success" (yellow alert)
130+
- `Failed` → "Has Error" (red X)
131+
132+
5. **Execution modal header** (`components/workflows/ExecutionModal.tsx`):
133+
- `Success` → green "Success"
134+
- `PartialSuccess` → yellow "Partial Success"
135+
- `Failed` → red "Has Error"
136+
137+
6. **Manual run callback** (`components/workflows/WorkflowControlButton.tsx`):
138+
Treats both `Success` and `PartialSuccess` as a successful run for
139+
the toast notification. This is the only place that already collapses
140+
the two into a single positive path.
141+
142+
### Target state (after SDK upgrade)
143+
144+
Once the SDK ships the `SUCCESS`/`FAILED`/`ERROR` redesign:
145+
146+
| `status` | Interpretation | UI treatment |
147+
|-----------|-----------------------------------------|---------------------------------------------|
148+
| `Success` | All executed steps passed (skips OK) | Green — "Simulation completed" |
149+
| `Failed` | At least one step failed | Red — "Simulation completed with errors" |
150+
| `Error` | System-level failure (VM crash, etc.) | Red — "Simulation failed — system error" |
151+
| `Pending` | Still running | Neutral — loading state |
152+
153+
**Branch skip detection** moves from status-level to the `steps` array:
154+
compare `steps.length` against the workflow's total node count. If
155+
`steps.length < nodeCount` and `status === Success`, display an
156+
informational note like "N steps skipped by condition".
157+
158+
### SDK/Client Migration Checklist
159+
160+
After upgrading `@avaprotocol/types`:
161+
162+
1. **Remove `isConditionalSkip` workaround** in `CanvasToolbarBottom``Success`
163+
already covers this case.
164+
2. **Remove `PartialSuccess` branches** from `StatusChips.tsx`,
165+
`ExecutionModal.tsx`, `getExecutionTimeDisplay()`.
166+
3. **Add `Error` handling** — new status for system-level failures.
167+
Display distinctly from `Failed` (e.g., "System error — contact support"
168+
vs "Execution failed — check step details").
169+
4. **Update simulation gate** (`app/types/simulation.ts`) — `Success` is the
170+
only passing status (same as today, but no ambiguity).
171+
5. **Update `WorkflowControlButton`** — remove `PartialSuccess` from the
172+
success path; add `Error` to the failure path.
173+
6. **Update Storybook mocks** — replace `ExecutionStatus.PartialSuccess`
174+
references with `Failed` or `Success` as appropriate.
175+
176+
## SDK Changes (ava-sdk-js)
177+
178+
**PR**: https://github.com/AvaProtocol/ava-sdk-js/pull/212
179+
180+
### TypeScript enum update
181+
182+
```typescript
183+
// packages/types/src/enums.ts
184+
export enum ExecutionStatus {
185+
Unspecified = "unspecified",
186+
Pending = "pending",
187+
Success = "success",
188+
Failed = "failed",
189+
Error = "error", // NEW — replaces PartialSuccess
190+
}
191+
```
192+
193+
### Backward compatibility
194+
195+
The SDK conversion functions handle legacy proto value `4` (retired
196+
`PARTIAL_SUCCESS`) by mapping it to `ExecutionStatus.Failed`:
197+
198+
```typescript
199+
case ProtobufExecutionStatus.EXECUTION_STATUS_FAILED:
200+
case 4 as ProtobufExecutionStatus: // legacy PARTIAL_SUCCESS
201+
return ExecutionStatus.Failed;
202+
```
203+
204+
This ensures stored executions written before the migration are
205+
displayed correctly without requiring a data migration.
206+
207+
### Test updates (18 files)
208+
209+
- All `ExecutionStatus.PartialSuccess` assertions replaced with
210+
`ExecutionStatus.Failed` (step failures) or `ExecutionStatus.Success`
211+
(branch skips)
212+
- `partialSuccess.test.ts` rewritten — test names and expectations
213+
aligned with the new three-value model
84214

85215
## Consequences
86216

@@ -90,3 +220,5 @@ enum ExecutionStatus {
90220
- Email summaries for branch-skip workflows now show a green success
91221
badge with a note like "3 nodes skipped by Branch condition" instead
92222
of a yellow warning badge.
223+
- Legacy stored executions with proto value `4` are transparently
224+
mapped to `Failed` by the SDK — no data migration required.

0 commit comments

Comments
 (0)