8000 fix(eval): delete current eval cases independently of tab mounting by Aaravkhanal · Pull Request #563 · google/adk-web · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/app/components/chat/chat.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,52 @@ describe('ChatComponent', () => {
await fixture.whenStable();
});

describe('deleting an eval case', () => {
beforeEach(() => {
component.appName = TEST_APP_1_NAME;
component.evalSetId = 'eval-set';
component.evalCase = {evalId: 'case-1'} as any;
mockDialog.open.and.returnValue({afterClosed: () => of(true)} as any);
});

it('deletes without a directly mounted eval tab and waits for success', () => {
const notify = spyOn(component as any, 'openSnackBar');
const refresh = spyOn(component.sidePanel(), 'refreshAfterEvalCaseDeleted');
(component as any).deleteEvalCase();
expect(refresh).not.toHaveBeenCalled();
expect(mockEvalService.deleteEvalCase).toHaveBeenCalledWith(
TEST_APP_1_NAME, 'eval-set', 'case-1');
expect(notify).not.toHaveBeenCalled();
mockEvalService.deleteEvalCaseResponse.next({});
expect(notify).toHaveBeenCalledWith('Eval case deleted', 'OK');
expect(component.evalCase).toBeNull();
expect(refresh).toHaveBeenCalledWith(TEST_APP_1_NAME, 'eval-set', 'case-1');
});

it('does not replace a case selected while deletion is pending', () => {
(component as any).deleteEvalCase();
component.evalCase = {evalId: 'case-2'} as any;
mockEvalService.deleteEvalCaseResponse.next({});
expect(component.evalCase?.evalId).toBe('case-2');
});

it('does not delete when confirmation is cancelled', () => {
mockDialog.open.and.returnValue({afterClosed: () => of(false)} as any);
(component as any).deleteEvalCase();
expect(mockEvalService.deleteEvalCase).not.toHaveBeenCalled();
expect(component.evalCase?.evalId).toBe('case-1');
});

it('keeps the case and reports a failed request', () => {
const notify = spyOn(component as any, 'openSnackBar');
(component as any).deleteEvalCase();
mockEvalService.deleteEvalCaseResponse.error(new Error('delete failed'));
expect(notify).toHaveBeenCalledWith('Failed to delete eval case', 'OK');
expect(notify).not.toHaveBeenCalledWith('Eval case deleted', 'OK');
expect(component.evalCase?.evalId).toBe('case-1');
});
});

describe('Component Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
Expand Down
25 changes: 20 additions & 5 deletions src/app/components/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3518,9 +3518,12 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
}

protected deleteEvalCase() {
const appName = this.appName;
const evalSetId = this.evalSetId;
const evalCaseId = this.evalCase!.evalId;
const dialogData: DeleteSessionDialogData = {
title: 'Confirm delete',
message: `Are you sure you want to delete ${this.evalCase!.evalId}?`,
message: `Are you sure you want to delete ${evalCaseId}?`,
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
};
Expand All @@ -3531,10 +3534,22 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
});

dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.evalTab()?.deleteEvalCase(this.evalCase!.evalId);
this.openSnackBar('Eval case deleted', 'OK');
}
if (!result) return;
// The eval tab belongs to SidePanelComponent and may not be mounted.
this.evalService.deleteEvalCase(appName, evalSetId, evalCaseId).subscribe({
next: () => {
if (this.appName === appName && this.evalSetId === evalSetId &&
this.evalCase?.evalId === evalCaseId) {
this.evalCase = null;
this.resetEditEvalCaseVars();
this.handleReturnToSession(true);
}
this.sidePanel().refreshAfterEvalCaseDeleted(
appName, evalSetId, evalCaseId);
this.openSnackBar('Eval case deleted', 'OK');
},
error: () => this.openSnackBar('Failed to delete eval case', 'OK'),
});
});
}

Expand Down
32 changes: 31 additions & 1 deletion src/app/components/eval-tab/eval-tab.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { EvalTabComponent } from './eval-tab.component';
import { EvalService } from '../../core/services/eval.service';
import { SessionService } from '../../core/services/session.service';
import { of, throwError } from 'rxjs';
import { of, Subject, throwError } from 'rxjs';
import {initTestBed} from '../../testing/utils';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import {
Expand Down Expand Up @@ -76,6 +77,7 @@ describe('EvalTabComponent', () => {
featureFlagService.isA2ACardEnabled.and.returnValue(of(false));
featureFlagService.isEvalV2Enabled.and.returnValue(of(false));

initTestBed(); // required for 1p compatibility
await TestBed.configureTestingModule({
imports: [MatDialogModule, EvalTabComponent, NoopAnimationsModule],
providers: [
Expand All @@ -99,6 +101,34 @@ describe('EvalTabComponent', () => {
fixture.detectChanges();
});

it('updates the table when the post-deletion list arrives', () => {
const response = new Subject<string[]>();
const service = TestBed.inject(EVAL_SERVICE) as jasmine.SpyObj<EvalService>;
service.listEvalCases.and.returnValue(response);
component.selectedEvalSet.set('set');
component.evalCases = ['case-1', 'case-2'];
component.selectedEvalCase.set({evalId: 'case-1'} as any);
const getEvalCase = spyOn(component as any, 'getEvalCase');

component.refreshAfterEvalCaseDeleted('case-1');
expect(component.selectedEvalCase()).toBeNull();
response.next(['case-2']);
expect(component.evalCases).toEqual(['case-2']);
expect(component.dataSource.data).toEqual(['case-2']);
expect(getEvalCase).toHaveBeenCalledWith('case-2');
});

it('preserves a different selected case during the refresh', () => {
const response = new Subject<string[]>();
const service = TestBed.inject(EVAL_SERVICE) as jasmine.SpyObj<EvalService>;
service.listEvalCases.and.returnValue(response);
component.evalCases = ['case-1', 'case-2'];
component.selectedEvalCase.set({evalId: 'case-2'} as any);
component.refreshAfterEvalCaseDeleted('case-1');
response.next(['case-2']);
expect(component.selectedEvalCase()?.evalId).toBe('case-2');
});

it('should create', () => {
expect(component).toBeTruthy();
});
Expand Down
14 changes: 10 additions & 4 deletions src/app/components/eval-tab/eval-tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,13 +763,19 @@ export class EvalTabComponent implements OnInit, OnChanges {
this.evalService
.deleteEvalCase(this.appName(), this.selectedEvalSet(), evalCaseId)
.subscribe((res) => {
this.deletedEvalCaseIndex = this.evalCases.indexOf(evalCaseId);
this.selectedEvalCase.set(null);
this.listEvalCases();
this.changeDetectorRef.detectChanges();
this.refreshAfterEvalCaseDeleted(evalCaseId);
});
}

refreshAfterEvalCaseDeleted(evalCaseId: string) {
const selectedCase = this.selectedEvalCase();
const replaceSelection = !selectedCase || selectedCase.evalId === evalCaseId;
this.deletedEvalCaseIndex = replaceSelection ? this.evalCases.indexOf(evalCaseId) : -1;
if (replaceSelection) this.selectedEvalCase.set(null);
// listEvalCases updates the view after the refreshed list arrives.
this.listEvalCases();
}

confirmDeleteEvalSet(event: Event, evalSetId: string) {
event.stopPropagation();
const dialogData: DeleteSessionDialogData = {
Expand Down
28 changes: 28 additions & 0 deletions src/app/components/side-panel/side-panel.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,34 @@ describe('SidePanelComponent', () => {
expect(component).toBeTruthy();
});

describe('refreshing a deleted eval case', () => {
it('refreshes the dynamically created tab for the matching eval set', () => {
const refresh = jasmine.createSpy('refreshAfterEvalCaseDeleted');
(component as any).evalTabRef.set({instance: {
appName: () => 'app', selectedEvalSet: () => 'set',
refreshAfterEvalCaseDeleted: refresh,
}});
component.refreshAfterEvalCaseDeleted('app', 'set', 'case');
expect(refresh).toHaveBeenCalledWith('case');
});

it('does not refresh a different eval set', () => {
const refresh = jasmine.createSpy('refreshAfterEvalCaseDeleted');
(component as any).evalTabRef.set({instance: {
appName: () => 'app', selectedEvalSet: () => 'other-set',
refreshAfterEvalCaseDeleted: refresh,
}});
component.refreshAfterEvalCaseDeleted('app', 'set', 'case');
expect(refresh).not.toHaveBeenCalled();
});

it('handles an unmounted eval tab', () => {
(component as any).evalTabRef.set(null);
expect(() => component.refreshAfterEvalCaseDeleted('app', 'set', 'case'))
.not.toThrow();
});
});

describe('Tab hiding', () => {
it('should hide Trace tab when isTraceEnabled is false', () => {
mockFeatureFlagService.isTraceEnabledResponse.next(false);
Expand Down
7 changes: 7 additions & 0 deletions src/app/components/side-panel/side-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ export class SidePanelComponent implements AfterViewInit, OnInit {
});
}

refreshAfterEvalCaseDeleted(appName: string, evalSetId: string, evalCaseId: string) {
const tab = this.evalTabRef()?.instance ?? this.evalTabComponent();
if (tab?.appName() === appName && tab.selectedEvalSet() === evalSetId) {
tab.refreshAfterEvalCaseDeleted(evalCaseId);
}
}

selectEvalCase(evalSetId: string, evalCase: EvalCase) {
const tab = this.evalTabComponent();
if (tab) {
Expand Down
Loading
0