Copilot commented on code in PR #6773:
URL: https://github.com/apache/texera/pull/6773#discussion_r3627710760


##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts:
##########
@@ -1035,4 +1035,333 @@ describe("HuggingFaceComponent (TestBed)", () => {
       expect(component.loading).toBe(false);
     });
   });
+
+  // ── Poll-path resolution (second instance waits on an in-flight fetch) ──
+  //
+  // The poll uses setInterval created inside ngOnInit (via detectChanges), 
which
+  // is a real timer here, so these use a real delay rather than 
fakeAsync/tick.
+
+  describe("poll-path resolution", () => {
+    /** Wait past one 200ms poll tick using real timers. */
+    const waitForPoll = () => new Promise<void>(resolve => setTimeout(resolve, 
300));

Review Comment:
   These tests use real timers (setTimeout + a 300ms wall-clock delay) to wait 
for the 200ms polling intervals. This makes the suite slower and can be flaky 
under CI load. The same spec file already tests polling using 
`fakeAsync`/`tick`/`discardPeriodicTasks` (e.g., the existing "polling" and 
"cleanup" sections), so these cases should be convertible to `fakeAsync` as 
well for deterministic timing.



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts:
##########
@@ -1035,4 +1035,333 @@ describe("HuggingFaceComponent (TestBed)", () => {
       expect(component.loading).toBe(false);
     });
   });
+
+  // ── Poll-path resolution (second instance waits on an in-flight fetch) ──
+  //
+  // The poll uses setInterval created inside ngOnInit (via detectChanges), 
which
+  // is a real timer here, so these use a real delay rather than 
fakeAsync/tick.
+
+  describe("poll-path resolution", () => {
+    /** Wait past one 200ms poll tick using real timers. */
+    const waitForPoll = () => new Promise<void>(resolve => setTimeout(resolve, 
300));
+
+    it("resolves both the task and model poll callbacks when the in-flight 
fetch succeeds", async () => {
+      // comp1 leaves BOTH tasks and models in flight.
+      const { field: field1 } = buildFieldWithFormGroup("text-generation");
+      component.field = field1;
+      fixture.detectChanges();
+      flushIconRequests();
+
+      // comp2 finds tasks + models in flight → enters BOTH poll paths.
+      const fixture2 = TestBed.createComponent(HuggingFaceComponent);
+      const component2 = fixture2.componentInstance;
+      const { field: field2 } = buildFieldWithFormGroup("text-generation");
+      component2.field = field2;
+      fixture2.detectChanges();
+      http.match(req => req.url.startsWith("assets/")).forEach(req => 
req.flush("<svg></svg>"));
+
+      expect(component2.tasksLoading).toBe(true);
+      expect(component2.loading).toBe(true);
+
+      // comp1's fetches complete → module-level caches are populated.
+      http.expectOne(`${API}/huggingface/tasks`).flush(buildTaskResponse());
+      http.expectOne(req => 
req.url.startsWith(`${API}/huggingface/models`)).flush(buildModels(7));
+
+      // Let the 200ms poll intervals observe the now-populated caches.
+      await waitForPoll();
+      flushIconRequests();
+
+      expect(component2.tasksLoading).toBe(false);
+      expect(component2.taskOptions).toEqual(buildTaskResponse());
+      expect((component2 as any).taskPollInterval).toBeNull();
+
+      expect(component2.loading).toBe(false);
+      expect(component2.pagedModels.length).toBe(7);
+      expect((component2 as any).modelPollInterval).toBeNull();
+
+      fixture2.destroy();
+    });
+
+    it("resolves both poll callbacks to the error state when the in-flight 
fetch fails", async () => {
+      const { field: field1 } = buildFieldWithFormGroup("text-generation");
+      component.field = field1;
+      fixture.detectChanges();
+      flushIconRequests();
+
+      const fixture2 = TestBed.createComponent(HuggingFaceComponent);
+      const component2 = fixture2.componentInstance;
+      const { field: field2 } = buildFieldWithFormGroup("text-generation");
+      component2.field = field2;
+      fixture2.detectChanges();
+      http.match(req => req.url.startsWith("assets/")).forEach(req => 
req.flush("<svg></svg>"));
+
+      expect(component2.tasksLoading).toBe(true);
+      expect(component2.loading).toBe(true);
+
+      // comp1's fetches fail → error caches are populated.
+      http.expectOne(`${API}/huggingface/tasks`).error(new 
ProgressEvent("error"));
+      http.expectOne(req => 
req.url.startsWith(`${API}/huggingface/models`)).error(new 
ProgressEvent("error"));
+
+      await waitForPoll();
+      flushIconRequests();
+
+      // Task poll falls back to the static list and surfaces the cached error.
+      expect(component2.tasksLoading).toBe(false);
+      expect(component2.taskOptions).toEqual(STATIC_TASK_OPTIONS);
+      expect(component2.tasksError).toBeTruthy();
+      expect((component2 as any).taskPollInterval).toBeNull();
+
+      // Model poll surfaces the cached error message.
+      expect(component2.loading).toBe(false);
+      expect(component2.errorMessage).toBeTruthy();
+      expect(component2.pagedModels.length).toBe(0);
+      expect((component2 as any).modelPollInterval).toBeNull();
+
+      fixture2.destroy();
+    });
+
+    it("stops polling and falls back when the in-flight fetch is invalidated 
before it resolves", async () => {
+      // comp1 leaves tasks + models in flight.
+      const { field: field1 } = buildFieldWithFormGroup("text-generation");
+      component.field = field1;
+      fixture.detectChanges();
+      flushIconRequests();
+
+      // comp2 enters both poll paths.
+      const fixture2 = TestBed.createComponent(HuggingFaceComponent);
+      const component2 = fixture2.componentInstance;
+      const { field: field2 } = buildFieldWithFormGroup("text-generation");
+      component2.field = field2;
+      fixture2.detectChanges();
+      http.match(req => req.url.startsWith("assets/")).forEach(req => 
req.flush("<svg></svg>"));
+
+      expect(component2.tasksLoading).toBe(true);
+      expect(component2.loading).toBe(true);
+
+      // Invalidate while comp1's fetches are still in flight: this 
unsubscribes the
+      // in-flight requests WITHOUT populating any cache, so the poll 
callbacks must
+      // detect the canceled fetch and fall back rather than wait forever.
+      invalidateHuggingFaceModelCache();
+
+      await waitForPoll();
+      flushIconRequests();
+
+      // Task poll detects the canceled fetch → stops and falls back to the 
static list.
+      expect(component2.tasksLoading).toBe(false);
+      expect(component2.taskOptions).toEqual(STATIC_TASK_OPTIONS);
+      expect((component2 as any).taskPollInterval).toBeNull();
+
+      // Model poll detects the canceled fetch → stops without an error.
+      expect(component2.loading).toBe(false);
+      expect((component2 as any).modelPollInterval).toBeNull();
+
+      fixture2.destroy();
+      // Drain the now-canceled comp1 requests so the shared afterEach 
verify() passes.
+      http.match(() => true);

Review Comment:
   `http.match(() => true)` is overly broad and can accidentally consume 
unrelated pending requests, which can mask regressions. Since this is meant to 
drain the canceled HuggingFace requests, match only those endpoints (e.g., 
`/huggingface/`).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to