This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new d561da8f3c test(frontend): add unit test coverage for AiAnalystService
(#6433)
d561da8f3c is described below
commit d561da8f3ca8b3ee6ce39b9a66320372d295f186
Author: Meng Wang <[email protected]>
AuthorDate: Tue Jul 14 16:29:31 2026 -0700
test(frontend): add unit test coverage for AiAnalystService (#6433)
### What changes were proposed in this PR?
Adds a Vitest unit-test suite for `AiAnalystService`
(`frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.ts`),
previously without a spec (~32% coverage). Uses
`HttpClientTestingModule` +
`HttpTestingController`; `WorkflowActionService` (an unused constructor
dependency) is stubbed. No backend.
9 tests cover:
- `isOpenAIEnabled` — GET `/aiassistant/isenabled` with `responseType:
"text"`; emits `true` for an `OpenAI` response, `false` for anything
else, `false` when the request errors, and short-circuits to the cached
flag without issuing a request.
- `sendPromptToOpenAI` — the manual `Observable` contract: emits `""`
(and skips the OpenAI call) when the assistant is disabled; POSTs `{
prompt }` to `/aiassistant/openai` and emits the trimmed completion when
enabled; emits `""` when the OpenAI request fails; and emits `""` when
the enablement check itself errors.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6430
### How was this PR tested?
New unit tests, run locally in `frontend/` (all green; the failure path
was
verified by deliberately breaking an assertion to confirm the suite goes
red):
```
ng test --watch=false --include
src/app/workspace/service/ai-analyst/ai-analyst.service.spec.ts
# Test Files 1 passed (1) | Tests 9 passed (9)
prettier --write <spec> # unchanged
eslint <spec> # clean
```
The suite is backed by `HttpClientTestingModule` /
`HttpTestingController`, so
it makes no real network calls (see `frontend/TESTING.md`).
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---
.../service/ai-analyst/ai-analyst.service.spec.ts | 134 +++++++++++++++++++++
1 file changed, 134 insertions(+)
diff --git
a/frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.spec.ts
b/frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.spec.ts
new file mode 100644
index 0000000000..174020402f
--- /dev/null
+++ b/frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.spec.ts
@@ -0,0 +1,134 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule, HttpTestingController } from
"@angular/common/http/testing";
+import { throwError } from "rxjs";
+import { AiAnalystService } from "./ai-analyst.service";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { AppSettings } from "../../../common/app-setting";
+
+describe("AiAnalystService", () => {
+ let service: AiAnalystService;
+ let httpMock: HttpTestingController;
+ const isEnabledUrl = `${AppSettings.getApiEndpoint()}/aiassistant/isenabled`;
+ const openaiUrl = `${AppSettings.getApiEndpoint()}/aiassistant/openai`;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ // WorkflowActionService is a constructor dependency but unused by the
tested
+ // methods, so an empty stub avoids pulling in the workflow-graph module.
+ providers: [AiAnalystService, { provide: WorkflowActionService,
useValue: {} }],
+ });
+ service = TestBed.inject(AiAnalystService);
+ httpMock = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpMock.verify();
+ vi.restoreAllMocks();
+ });
+
+ it("should be created", () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe("isOpenAIEnabled", () => {
+ it("GETs /aiassistant/isenabled as text and emits true when the backend
returns OpenAI", () => {
+ let result: boolean | undefined;
+ service.isOpenAIEnabled().subscribe(v => (result = v));
+
+ const req = httpMock.expectOne(isEnabledUrl);
+ expect(req.request.method).toBe("GET");
+ expect(req.request.responseType).toBe("text");
+
+ req.flush("OpenAI");
+ expect(result).toBe(true);
+ });
+
+ it("emits false for any non-OpenAI response", () => {
+ let result: boolean | undefined;
+ service.isOpenAIEnabled().subscribe(v => (result = v));
+
+ httpMock.expectOne(isEnabledUrl).flush("NoAiAssistant");
+ expect(result).toBe(false);
+ });
+
+ it("emits false when the request errors", () => {
+ let result: boolean | undefined;
+ service.isOpenAIEnabled().subscribe(v => (result = v));
+
+ httpMock.expectOne(isEnabledUrl).flush("boom", { status: 500,
statusText: "Server Error" });
+ expect(result).toBe(false);
+ });
+
+ it("returns the cached flag without issuing a request", () => {
+ (service as any).isAIAssistantEnabled = true;
+ let result: boolean | undefined;
+ service.isOpenAIEnabled().subscribe(v => (result = v));
+
+ expect(result).toBe(true);
+ httpMock.expectNone(isEnabledUrl);
+ });
+ });
+
+ describe("sendPromptToOpenAI", () => {
+ it("emits an empty string without calling OpenAI when the assistant is
disabled", () => {
+ let result: string | undefined;
+ service.sendPromptToOpenAI("hello").subscribe(v => (result = v));
+
+ httpMock.expectOne(isEnabledUrl).flush("NoAiAssistant");
+ httpMock.expectNone(openaiUrl);
+ expect(result).toBe("");
+ });
+
+ it("POSTs the prompt and emits the trimmed completion when enabled", () =>
{
+ let result: string | undefined;
+ service.sendPromptToOpenAI("analyze this").subscribe(v => (result = v));
+
+ httpMock.expectOne(isEnabledUrl).flush("OpenAI");
+
+ const req = httpMock.expectOne(openaiUrl);
+ expect(req.request.method).toBe("POST");
+ expect(req.request.body).toEqual({ prompt: "analyze this" });
+ req.flush({ choices: [{ message: { content: " the answer " } }] });
+
+ expect(result).toBe("the answer");
+ });
+
+ it("emits an empty string when the OpenAI request fails", () => {
+ let result: string | undefined;
+ service.sendPromptToOpenAI("x").subscribe(v => (result = v));
+
+ httpMock.expectOne(isEnabledUrl).flush("OpenAI");
+ httpMock.expectOne(openaiUrl).flush("boom", { status: 500, statusText:
"Server Error" });
+
+ expect(result).toBe("");
+ });
+
+ it("emits an empty string when the enablement check errors", () => {
+ vi.spyOn(service, "isOpenAIEnabled").mockReturnValue(throwError(() =>
new Error("check failed")));
+ let result: string | undefined;
+ service.sendPromptToOpenAI("x").subscribe(v => (result = v));
+
+ expect(result).toBe("");
+ });
+ });
+});