google / google/adk-python

[Bug] Delete Eval Case shows "Eval case deleted" snackbar but HTTP DELETE is never called when evalTab() signal is null

ใ‚ชใƒผใƒ—ใƒณ
#4,900 ใ‚ณใƒกใƒณใƒˆ 2 ไปถ ใƒชใ‚ขใ‚ฏใ‚ทใƒงใƒณ 0 ไปถ ๆ‹…ๅฝ“่€… 2 ๅ @Jacksunwei ใŒๆ‹…ๅฝ“ใ‚’ๅธŒๆœ›ใ—ใฆใ„ใพใ™ GitHub ใง่ฆ‹ใ‚‹
eval needs review
ไธป่ฆ่จ€่ชž
Python
ใ‚นใ‚ฟใƒผ
21.5k
ใƒ•ใ‚ฉใƒผใ‚ฏ
4k
ๅนณๅ‡ใƒžใƒผใ‚ธ
1ๆ—ฅ 14ๆ™‚้–“
ใƒžใƒผใ‚ธๆธˆใฟ PR๏ผˆ30ๆ—ฅ๏ผ‰
37

่ชฌๆ˜Ž

## ๐Ÿ”ด Required Information
*Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A*

**Describe the Bug:**
When deleting an eval case from the eval case detail view in adk web, the UI shows a "Eval case deleted" success snackbar but the HTTP DELETE request is never actually made. The eval case remains in the eval set JSON file and still appears in the list after the page refreshes.

**Steps to Reproduce:**
1. Run adk web for any agent that has eval sets configured
2. Open an eval set from the left panel
3. Click on a specific eval case to open its detail view
4. Click the delete (trash) icon for the eval case
5. Confirm deletion in the confirmation dialog
6. Observe: snackbar says "Eval case deleted" โœ…
7. Observe: the eval case is still in the list โŒ

**Expected Behavior:**
The HTTP DELETE request is sent to /apps/{app_name}/eval_sets/{eval_set_id}/evals/{eval_id} and the eval case is permanently removed from the eval set.

**Observed Behavior:**
No HTTP DELETE request is made. The case persists in the JSON file. The success snackbar is misleading โ€” it fires even when deletion was skipped.

**Environment Details:**
โ€ข Google ADK version: 1.27.2
โ€ข Angular bundle: main-7SJG752M.js (found in site-packages/google/adk/cli/browser/)
โ€ข OS: Windows 11
โ€ข Browser: (Chrome / Edge / Firefox)
โ€ข Python: 3.13

Root Cause (Found via bundle analysis)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

In the eval case detail view component, the delete confirmation handler uses optional chaining (?.) combined with the comma operator:

javascript
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
dialog.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.evalTab()?.deleteEvalCase(this.evalCase.evalId), // optional chain
this.openSnackBar("Eval case deleted", "OK") // comma operator โ€” always fires!
}
})
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

evalTab() is a signal/ViewChild reference pointing to the eval cases list/tab component. When the user is viewing the detail panel, the list panel may be conditionally unmounted by Angular, causing evalTab() to return null or undefined.

When evalTab() is null:
โ€ข this.evalTab()?.deleteEvalCase(...) โ†’ silently skipped (optional chaining)
โ€ข this.openSnackBar("Eval case deleted", "OK") โ†’ always executes (comma operator)

Result: the user sees a success message for an operation that never happened.

Secondary Issue โ€” Race Condition on Successful Delete
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Even when evalTab() is NOT null and the DELETE does fire, there is a race condition in the tab component's delete callback:

javascript
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
this.evalService.deleteEvalCase(...).subscribe(response => {
this.deletedEvalCaseIndex = this.evalCases.indexOf(e),
this.selectedEvalCase.set(null),
this.listEvalCases(), // async HTTP GET โ€” not awaited
this.changeDetectorRef.detectChanges() // fires BEFORE listEvalCases() resolves
})
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

detectChanges() runs synchronously and re-renders the UI with the stale old list before the async listEvalCases() GET response arrives.

Suggested Fix
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Issue 1: The snackbar should only be shown after the HTTP DELETE completes successfully, and should not rely on the comma operator pattern:

javascript
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Instead of comma operator pattern:
if (confirmed && this.evalTab()) {
this.evalTab()!.deleteEvalCase(this.evalCase.evalId)
.pipe(/* success */)
.subscribe(() => this.openSnackBar("Eval case deleted", "OK"))
} else if (confirmed && !this.evalTab()) {
// Fallback: call the service directly without going through evalTab()
}
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Issue 2: detectChanges() should be called inside the listEvalCases() subscription callback after the list has been refreshed, not immediately after calling it.

**Model Information:**

- Are you using LiteLLM: Yes
- Which model is being used: gemini-2.5-flash

---

## ๐ŸŸก Optional Information
*Providing this information greatly speeds up the resolution process.*

**Regression:**
Did this work in a previous version of ADK? If so, which one?
No. Didnt work in previous versions too.

**Logs:**
Please attach relevant logs. Wrap them in code blocks (```) or attach a text file.
```text
NA
```

**Screenshots / Video:**
NA

**Additional Context:**
Workaround
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Until this is fixed, delete eval cases directly from the eval set JSON
files located at:
//.evalset.json

Remove the relevant entry from the eval_cases array and save the file.

**Minimal Reproduction Code:**
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
```python
NA
```

**How often has this issue occurred?:**
- Always (100%)

ใ‚ณใƒณใƒˆใƒชใƒ“ใƒฅใƒผใ‚ทใƒงใƒณใ‚ฌใ‚คใƒ‰

ใ‚ณใƒณใƒˆใƒชใƒ“ใƒฅใƒผใ‚ทใƒงใƒณใ‚ฌใ‚คใƒ‰ใ‚’้–‹ใ

่ฉ•ไพก

ใ“ใฎ issue ใฏใพใ ่ฉ•ไพกใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚

ๆ–ฐใ—ใ„ issue ใ‚’ใƒกใƒผใƒซใงๅ—ใ‘ๅ–ใ‚‹

ๅˆๅฟƒ่€…ๅ‘ใ‘ใฎ GitHub issue ใ‚’็Ÿญใใพใจใ‚ใŸใƒ€ใ‚คใ‚ธใ‚งใ‚นใƒˆใ€‚