This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch goal-test
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git

commit ed756e916caa5a39f7cabd5b62639cb185a81183
Author: imbajin <[email protected]>
AuthorDate: Sun May 31 02:11:15 2026 +0800

    test(llm): expand deterministic contract coverage
    
    - add fake LLM fixtures for parser and operator tests
    - cover malformed parser, API, and provider wrapper contracts
    - apply regression-backed parser and API fixes
---
 .../checkpoints/05-parser-api-operator.md          |  75 +++++++++++++
 .workflow/quality-program/quality-state.json       |  32 +++++-
 .../reports/production-change-ledger.md            |   1 +
 .../2026-05-31-hugegraph-ai-quality-program.md     |  18 +--
 hugegraph-llm/src/hugegraph_llm/api/rag_api.py     |   2 +-
 hugegraph-llm/src/tests/api/test_rag_api.py        | 105 ++++++++++++++++++
 hugegraph-llm/src/tests/fixtures/fake_llm.py       |  31 ++++++
 .../models/embeddings/test_ollama_embedding.py     |  23 ++++
 .../src/tests/models/llms/test_litellm_client.py   |  21 +++-
 .../src/tests/models/llms/test_openai_client.py    |  12 ++
 .../operators/llm_op/test_gremlin_generate.py      |  25 +++++
 .../tests/operators/llm_op/test_keyword_extract.py |  27 +++++
 .../llm_op/test_property_graph_extract.py          | 122 +++++++++++++++++++++
 13 files changed, 477 insertions(+), 17 deletions(-)

diff --git a/.workflow/quality-program/checkpoints/05-parser-api-operator.md 
b/.workflow/quality-program/checkpoints/05-parser-api-operator.md
new file mode 100644
index 00000000..3532a3a7
--- /dev/null
+++ b/.workflow/quality-program/checkpoints/05-parser-api-operator.md
@@ -0,0 +1,75 @@
+# G4 Parser API Operator Checkpoint
+
+## Status
+
+Complete.
+
+## Completed Scope
+
+- Added deterministic `FakeLLM` fixture for parser and operator tests.
+- Expanded property graph parser contracts for fenced JSON, malformed JSON, 
numeric vertex IDs, and duplicate vertices/edges.
+- Expanded keyword parser contracts for fenced output, empty provider output, 
duplicate keywords, malformed items, and non-keyword provider text.
+- Expanded Gremlin generator contracts for explanation-plus-code, multiple 
candidate blocks, and empty deterministic LLM output.
+- Expanded public FastAPI route contracts for graph, LLM, embedding, reranker 
config mapping, invalid request validation, and callback exception response 
shape.
+- Expanded provider wrapper contracts for OpenAI malformed SDK responses, 
LiteLLM success/malformed responses, and Ollama missing `embed` methods.
+- No real LLM, embedding, reranker, vector DB, or UI credentials were required.
+
+## Red Test Observed
+
+```text
+test_llm_config_api_passes_openai_fields_to_apply_llm_conf
+  failed with ValueError: "LLMConfig" object has no field "llm_type"
+```
+
+## Production Fix
+
+| File | Change | Proving test |
+|---|---|---|
+| `hugegraph-llm/src/hugegraph_llm/api/rag_api.py` | Map `/config/llm` to 
existing `llm_settings.chat_llm_type` instead of nonexistent 
`llm_settings.llm_type`. | `uv run pytest hugegraph-llm/src/tests/api -m "unit 
or contract" -v --tb=short` |
+
+## Commands Run
+
+```bash
+uv run ruff format hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py 
hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
+uv run pytest hugegraph-llm/src/tests/operators/llm_op -m "unit or contract" 
-v --tb=short
+uv run pytest hugegraph-llm/src/tests/api -m "unit or contract" -v --tb=short
+uv run pytest hugegraph-llm/src/tests/models -m "unit or contract" -v 
--tb=short
+uv run ruff format --check hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py hugegraph [...]
+uv run ruff check hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py hugegraph-llm/src/ 
[...]
+git diff --check
+```
+
+## Verification Result
+
+| Layer | Command | Result |
+|---|---|---|
+| LLM parser/operator contracts | `uv run pytest 
hugegraph-llm/src/tests/operators/llm_op -m "unit or contract" -v --tb=short` | 
`68 passed` |
+| API public contracts | `uv run pytest hugegraph-llm/src/tests/api -m "unit 
or contract" -v --tb=short` | `6 passed` |
+| Provider wrapper contracts | `uv run pytest hugegraph-llm/src/tests/models 
-m "unit or contract" -v --tb=short` | `26 passed, 2 skipped, 17 deselected` |
+| Formatting | `uv run ruff format --check ...` | `9 files already formatted` |
+| Lint | `uv run ruff check ...` | `All checks passed` |
+
+## Files Touched
+
+- `.workflow/quality-program/checkpoints/05-parser-api-operator.md`
+- `.workflow/quality-program/quality-state.json`
+- `.workflow/quality-program/reports/production-change-ledger.md`
+- `docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md`
+- `hugegraph-llm/src/hugegraph_llm/api/rag_api.py`
+- `hugegraph-llm/src/tests/fixtures/fake_llm.py`
+- `hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py`
+- `hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py`
+- `hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py`
+- `hugegraph-llm/src/tests/api/test_rag_api.py`
+- `hugegraph-llm/src/tests/models/llms/test_openai_client.py`
+- `hugegraph-llm/src/tests/models/llms/test_litellm_client.py`
+- `hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py`
+
+## Failure Classification
+
+- API field mapping contract gap: `/config/llm` targeted a nonexistent 
Pydantic settings field.
+- No parser, operator, provider-wrapper, or external credential blocker 
remains in G4.
+
+## Next Goal Readiness
+
+G5 can begin. Deterministic parser, API, and provider wrapper suites are 
available without external credentials.
diff --git a/.workflow/quality-program/quality-state.json 
b/.workflow/quality-program/quality-state.json
index fd83550b..4b1973d5 100644
--- a/.workflow/quality-program/quality-state.json
+++ b/.workflow/quality-program/quality-state.json
@@ -1,5 +1,5 @@
 {
-  "current_goal": "G4",
+  "current_goal": "G5",
   "repo_sha_start": "d048bc9fa07042833549dd1def5ff3222c47be70",
   "base_branch": "main",
   "open_pr_snapshot_time": "2026-05-30T17:18:30Z",
@@ -8,7 +8,8 @@
     "G0",
     "G1",
     "G2",
-    "G3"
+    "G3",
+    "G4"
   ],
   "files_touched": [
     ".workflow/quality-program/README.md",
@@ -62,6 +63,7 @@
     ".workflow/quality-program/checkpoints/02-service-fixture.md",
     ".workflow/quality-program/checkpoints/03-client-contract.md",
     ".workflow/quality-program/checkpoints/04-llm-boundary.md",
+    ".workflow/quality-program/checkpoints/05-parser-api-operator.md",
     ".workflow/quality-program/coverage/client-g2.json",
     ".workflow/quality-program/reports/production-change-ledger.md",
     "docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md",
@@ -70,7 +72,10 @@
     "hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py",
     
"hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py",
     "hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py",
-    "hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py"
+    "hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py",
+    "hugegraph-llm/src/hugegraph_llm/api/rag_api.py",
+    "hugegraph-llm/src/tests/fixtures/fake_llm.py",
+    "hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py"
   ],
   "production_changes": [
     {
@@ -84,6 +89,12 @@
       "file": 
"hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py",
       "change": "Map primary-key label:value edge endpoints to server-created 
VIDs and raise explicit vertex creation errors before accessing id.",
       "test": "uv run pytest 
hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py::TestCommit2Graph::test_load_into_graph_raises_explicit_error_when_vertex_creation_fails
 -q; HUGEGRAPH_REQUIRED=true uv run pytest 
hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py -v --tb=short"
+    },
+    {
+      "goal": "G4",
+      "file": "hugegraph-llm/src/hugegraph_llm/api/rag_api.py",
+      "change": "Map /config/llm to llm_settings.chat_llm_type instead of 
nonexistent llm_settings.llm_type.",
+      "test": "uv run pytest hugegraph-llm/src/tests/api -m 'unit or contract' 
-v --tb=short"
     }
   ],
   "tests_added_or_changed": [
@@ -93,7 +104,10 @@
     "Updated client integration harness so hugegraph-marked legacy tests use 
the service fixture gate before unittest setUpClass.",
     "Added real HugeGraph schema CRUD, graph ID, and Gremlin error-surface 
contract tests for G2.",
     "Added LLM HugeGraph boundary integration tests for SchemaManager, 
Commit2Graph, FetchGraphData, and GremlinExecuteNode.",
-    "Marked existing hugegraph_op unittest files as unit so strict marker 
selection exercises them."
+    "Marked existing hugegraph_op unittest files as unit so strict marker 
selection exercises them.",
+    "Added FakeLLM fixture and deterministic parser/operator tests for 
property graph, keyword, and Gremlin generation contracts.",
+    "Added API public-surface tests for config field mapping, invalid request 
bodies, and callback exception responses.",
+    "Added provider wrapper tests for OpenAI, LiteLLM, and Ollama 
malformed/error surfaces."
   ],
   "commands_run": [
     "sed -n '1,220p' AGENTS.md",
@@ -154,7 +168,13 @@
     "uv run pytest hugegraph-llm/src/tests/operators/hugegraph_op -m 'unit or 
contract' -q",
     "uv run ruff format --check hugegraph-llm/src/tests/conftest.py 
hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py 
hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py",
     "uv run ruff check hugegraph-llm/src/tests/conftest.py 
hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py 
hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py 
hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py",
-    "uv run pytest hugegraph-llm/src/tests --collect-only -q"
+    "uv run pytest hugegraph-llm/src/tests --collect-only -q",
+    "uv run ruff format hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py 
hugegraph-llm/src/tests/models/embeddings/test_ollama_embed [...]
+    "uv run pytest hugegraph-llm/src/tests/operators/llm_op -m 'unit or 
contract' -v --tb=short",
+    "uv run pytest hugegraph-llm/src/tests/api -m 'unit or contract' -v 
--tb=short",
+    "uv run pytest hugegraph-llm/src/tests/models -m 'unit or contract' -v 
--tb=short",
+    "uv run ruff format --check hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py huge [...]
+    "uv run ruff check hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
hugegraph-llm/src/tests/fixtures/fake_llm.py 
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
hugegraph-llm/src/tests/api/test_rag_api.py 
hugegraph-llm/src/tests/models/llms/test_openai_client.py 
hugegraph-llm/src/tests/models/llms/test_litellm_client.py hugegraph-llm [...]
   ],
   "known_failures": [],
   "deferred_items": [
@@ -164,5 +184,5 @@
     "Avoid #240 vector/property embedding abstraction surface.",
     "Quarantine client/test fixes overlapping #323/#329/#342 until exact 
changed files are inspected."
   ],
-  "next_recommended_action": "Start G4 deterministic parser, API, and operator 
coverage"
+  "next_recommended_action": "Start G5 core RAG, KG, and Text2Gremlin smoke 
gates"
 }
diff --git a/.workflow/quality-program/reports/production-change-ledger.md 
b/.workflow/quality-program/reports/production-change-ledger.md
index 1fbe4888..82893855 100644
--- a/.workflow/quality-program/reports/production-change-ledger.md
+++ b/.workflow/quality-program/reports/production-change-ledger.md
@@ -4,3 +4,4 @@
 |---|---|---|---|---|---|
 | G2 | `hugegraph-python-client/src/pyhugegraph/utils/util.py` | Preserve 
backend error envelope details for non-404 HTTP errors and prefer server 
`message` over `exception`. | `uv run pytest 
hugegraph-python-client/src/tests/api/test_response_validation.py -q` | 
Response validation contract gap for 500 backend envelopes. | Low; narrows 
error wrapping to preserve parsed server details while keeping 404 mapping. |
 | G3 | 
`hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py` 
| Map primary-key `label:value` edge endpoints to server-created VIDs and raise 
explicit vertex creation errors before accessing `.id`. | `uv run pytest 
hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py::TestCommit2Graph::test_load_into_graph_raises_explicit_error_when_vertex_creation_fails
 -q`; `HUGEGRAPH_REQUIRED=true uv run pytest 
hugegraph-llm/src/tests/integration/test_hug [...]
+| G4 | `hugegraph-llm/src/hugegraph_llm/api/rag_api.py` | Map `/config/llm` to 
existing `llm_settings.chat_llm_type` instead of nonexistent 
`llm_settings.llm_type`. | `uv run pytest hugegraph-llm/src/tests/api -m "unit 
or contract" -v --tb=short` | API public-surface field mapping contract gap. | 
Low; aligns route with existing config schema and leaves callback arguments 
unchanged. |
diff --git a/docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md 
b/docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md
index 0e9ecce8..c37df9cf 100644
--- a/docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md
+++ b/docs/superpowers/plans/2026-05-31-hugegraph-ai-quality-program.md
@@ -1061,7 +1061,7 @@ git commit -m "test(llm): harden hugegraph boundary 
coverage" -m "- add real Hug
 - Create: `hugegraph-llm/src/tests/fixtures/fake_llm.py`
 - Create/Update: 
`.workflow/quality-program/checkpoints/05-parser-api-operator.md`
 
-- [ ] **Step G4.1: Add deterministic fake LLM fixture**
+- [x] **Step G4.1: Add deterministic fake LLM fixture**
 
 Create `hugegraph-llm/src/tests/fixtures/fake_llm.py`:
 
@@ -1081,7 +1081,7 @@ class FakeLLM:
         return self.generate(prompt=prompt, messages=messages, **kwargs)
 ```
 
-- [ ] **Step G4.2: Add adversarial graph JSON parser tests**
+- [x] **Step G4.2: Add adversarial graph JSON parser tests**
 
 In `test_property_graph_extract.py`, add cases for:
 
@@ -1108,7 +1108,7 @@ def test_property_graph_extract_strips_fenced_json():
     assert result == []
 ```
 
-- [ ] **Step G4.3: Add keyword parser malformed output tests**
+- [x] **Step G4.3: Add keyword parser malformed output tests**
 
 In `test_keyword_extract.py`, add tests for fenced output, empty output, 
duplicate keywords, and non-list provider text.
 
@@ -1120,7 +1120,7 @@ Expected assertions:
 - markdown fences do not leak into keywords
 ```
 
-- [ ] **Step G4.4: Add Gremlin-only contract tests**
+- [x] **Step G4.4: Add Gremlin-only contract tests**
 
 In `test_gremlin_generate.py`, add tests for:
 
@@ -1133,7 +1133,7 @@ In `test_gremlin_generate.py`, add tests for:
 
 Assert the public contract expected by callers: Gremlin-only string or 
explicit failure.
 
-- [ ] **Step G4.5: Expand API public-surface tests**
+- [x] **Step G4.5: Expand API public-surface tests**
 
 In `hugegraph-llm/src/tests/api/test_rag_api.py`, add FastAPI TestClient tests 
for:
 
@@ -1148,7 +1148,7 @@ In `hugegraph-llm/src/tests/api/test_rag_api.py`, add 
FastAPI TestClient tests f
 
 Do not test private helper calls when the public route can prove the contract.
 
-- [ ] **Step G4.6: Expand provider wrapper error tests**
+- [x] **Step G4.6: Expand provider wrapper error tests**
 
 For OpenAI, LiteLLM, Ollama, embedding, and reranker wrappers:
 
@@ -1162,7 +1162,7 @@ For OpenAI, LiteLLM, Ollama, embedding, and reranker 
wrappers:
 
 Use mocked SDK calls only. Do not use real credentials.
 
-- [ ] **Step G4.7: Run deterministic LLM contract suites**
+- [x] **Step G4.7: Run deterministic LLM contract suites**
 
 Run:
 
@@ -1174,7 +1174,7 @@ uv run pytest hugegraph-llm/src/tests/models -m "unit or 
contract" -v --tb=short
 
 Expected: no external calls.
 
-- [ ] **Step G4.8: Apply minimal parser/API/operator fixes**
+- [x] **Step G4.8: Apply minimal parser/API/operator fixes**
 
 Allowed fixes:
 
@@ -1187,7 +1187,7 @@ Allowed fixes:
 
 Every fix requires a regression test added in this goal.
 
-- [ ] **Step G4.9: Write checkpoint and commit**
+- [x] **Step G4.9: Write checkpoint and commit**
 
 Run:
 
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index 76217251..bce7de0d 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -161,7 +161,7 @@ def rag_http_api(
     # TODO: restructure the implement of llm to three types, like 
"/config/chat_llm"
     @router.post("/config/llm", status_code=status.HTTP_201_CREATED)
     def llm_config_api(req: LLMConfigRequest):
-        llm_settings.llm_type = req.llm_type
+        llm_settings.chat_llm_type = req.llm_type
 
         if req.llm_type == "openai":
             res = apply_llm_conf(
diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py 
b/hugegraph-llm/src/tests/api/test_rag_api.py
index 789da403..336216fb 100644
--- a/hugegraph-llm/src/tests/api/test_rag_api.py
+++ b/hugegraph-llm/src/tests/api/test_rag_api.py
@@ -26,6 +26,24 @@ from hugegraph_llm.api.rag_api import rag_http_api
 pytestmark = pytest.mark.contract
 
 
+def _make_test_client(**overrides):
+    callbacks = {
+        "rag_answer_func": Mock(return_value=("raw", "vector", "graph", 
"graph_vector")),
+        "graph_rag_recall_func": Mock(return_value={"query": "q", "keywords": 
[]}),
+        "apply_graph_conf": Mock(return_value=status.HTTP_200_OK),
+        "apply_llm_conf": Mock(return_value=status.HTTP_200_OK),
+        "apply_embedding_conf": Mock(return_value=status.HTTP_200_OK),
+        "apply_reranker_conf": Mock(return_value=status.HTTP_200_OK),
+        "gremlin_generate_selective_func": Mock(return_value={"result": 
"g.V()"}),
+    }
+    callbacks.update(overrides)
+    router = APIRouter()
+    rag_http_api(router, **callbacks)
+    app = FastAPI()
+    app.include_router(router)
+    return TestClient(app), callbacks
+
+
 def test_graph_config_api_passes_graph_field_to_apply_graph_conf():
     apply_graph_conf = Mock(return_value=status.HTTP_200_OK)
     router = APIRouter()
@@ -63,3 +81,90 @@ def 
test_graph_config_api_passes_graph_field_to_apply_graph_conf():
         "space_a",
         origin_call="http",
     )
+
+
+def test_llm_config_api_passes_openai_fields_to_apply_llm_conf():
+    client, callbacks = _make_test_client()
+
+    response = client.post(
+        "/config/llm",
+        json={
+            "llm_type": "openai",
+            "api_key": "sk-test",
+            "api_base": "https://api.example.test";,
+            "language_model": "gpt-test",
+            "max_tokens": "1024",
+        },
+    )
+
+    assert response.status_code == status.HTTP_201_CREATED
+    assert response.json() == {"message": "Connection successful. Configured 
finished."}
+    callbacks["apply_llm_conf"].assert_called_once_with(
+        "sk-test",
+        "https://api.example.test";,
+        "gpt-test",
+        "1024",
+        origin_call="http",
+    )
+
+
+def test_embedding_config_api_passes_openai_fields_to_apply_embedding_conf():
+    client, callbacks = _make_test_client()
+
+    response = client.post(
+        "/config/embedding",
+        json={
+            "llm_type": "openai",
+            "api_key": "sk-embedding",
+            "api_base": "https://embedding.example.test";,
+            "language_model": "embedding-test",
+        },
+    )
+
+    assert response.status_code == status.HTTP_201_CREATED
+    callbacks["apply_embedding_conf"].assert_called_once_with(
+        "sk-embedding",
+        "https://embedding.example.test";,
+        "embedding-test",
+        origin_call="http",
+    )
+
+
+def test_rerank_config_api_passes_cohere_fields_to_apply_reranker_conf():
+    client, callbacks = _make_test_client()
+
+    response = client.post(
+        "/config/rerank",
+        json={
+            "reranker_type": "cohere",
+            "api_key": "cohere-key",
+            "reranker_model": "rerank-test",
+            "cohere_base_url": "https://cohere.example.test";,
+        },
+    )
+
+    assert response.status_code == status.HTTP_201_CREATED
+    callbacks["apply_reranker_conf"].assert_called_once_with(
+        "cohere-key",
+        "rerank-test",
+        "https://cohere.example.test";,
+        origin_call="http",
+    )
+
+
+def test_rag_api_invalid_request_body_returns_validation_shape():
+    client, _ = _make_test_client()
+
+    response = client.post("/rag", json={})
+
+    assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+    assert response.json()["detail"][0]["loc"][-1] == "query"
+
+
+def test_text2gremlin_callback_exception_returns_stable_response():
+    client, _ = 
_make_test_client(gremlin_generate_selective_func=Mock(side_effect=RuntimeError("callback
 failed")))
+
+    response = client.post("/text2gremlin", json={"query": "find people"})
+
+    assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+    assert response.json() == {"detail": "An unexpected error occurred during 
Gremlin generation."}
diff --git a/hugegraph-llm/src/tests/fixtures/fake_llm.py 
b/hugegraph-llm/src/tests/fixtures/fake_llm.py
new file mode 100644
index 00000000..4eacd807
--- /dev/null
+++ b/hugegraph-llm/src/tests/fixtures/fake_llm.py
@@ -0,0 +1,31 @@
+# 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.
+
+
+class FakeLLM:
+    def __init__(self, responses):
+        self.responses = list(responses)
+        self.calls = []
+
+    def generate(self, prompt=None, messages=None, **kwargs):
+        self.calls.append({"prompt": prompt, "messages": messages, "kwargs": 
kwargs})
+        if not self.responses:
+            raise AssertionError("FakeLLM has no remaining responses")
+        return self.responses.pop(0)
+
+    async def agenerate(self, prompt=None, messages=None, **kwargs):
+        return self.generate(prompt=prompt, messages=messages, **kwargs)
diff --git a/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py 
b/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
index e7a2702a..3aca1b9a 100644
--- a/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
+++ b/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
@@ -20,9 +20,13 @@ import os
 import unittest
 from unittest.mock import AsyncMock, MagicMock
 
+import pytest
+
 from hugegraph_llm.models.embeddings.base import SimilarityMode
 from hugegraph_llm.models.embeddings.ollama import OllamaEmbedding
 
+pytestmark = pytest.mark.contract
+
 
 class TestOllamaEmbedding(unittest.TestCase):
     def setUp(self):
@@ -82,6 +86,13 @@ class TestOllamaEmbedding(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "missing 'embeddings'"):
             ollama_embedding.get_texts_embeddings(["a"])
 
+    def test_get_texts_embeddings_requires_embed_method(self):
+        ollama_embedding = OllamaEmbedding(model="test-model")
+        ollama_embedding.client = object()
+
+        with self.assertRaisesRegex(AttributeError, "required 'embed' method"):
+            ollama_embedding.get_texts_embeddings(["a"])
+
     def test_get_texts_embeddings_requires_non_empty_embeddings(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.client = MagicMock()
@@ -102,3 +113,15 @@ class TestOllamaEmbedding(unittest.TestCase):
         import asyncio
 
         asyncio.run(run_async_test())
+
+    def test_async_get_text_embedding_requires_embed_method(self):
+        ollama_embedding = OllamaEmbedding(model="test-model")
+        ollama_embedding.async_client = object()
+
+        async def run_async_test():
+            with self.assertRaisesRegex(AttributeError, "required 'embed' 
method"):
+                await ollama_embedding.async_get_text_embedding("a")
+
+        import asyncio
+
+        asyncio.run(run_async_test())
diff --git a/hugegraph-llm/src/tests/models/llms/test_litellm_client.py 
b/hugegraph-llm/src/tests/models/llms/test_litellm_client.py
index f8ff89f6..c32cb128 100644
--- a/hugegraph-llm/src/tests/models/llms/test_litellm_client.py
+++ b/hugegraph-llm/src/tests/models/llms/test_litellm_client.py
@@ -17,7 +17,7 @@
 
 import asyncio
 import unittest
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, MagicMock, patch
 
 import pytest
 from litellm.exceptions import APIError, BudgetExceededError
@@ -28,6 +28,25 @@ pytestmark = pytest.mark.contract
 
 
 class TestLiteLLMClient(unittest.TestCase):
+    def test_generate_returns_message_content(self):
+        client = LiteLLMClient(model_name="openai/gpt-4.1-mini")
+        response = MagicMock()
+        response.usage = {"prompt_tokens": 1}
+        response.choices = [MagicMock(message=MagicMock(content="hello"))]
+
+        with patch("hugegraph_llm.models.llms.litellm.completion", 
return_value=response) as mock_completion:
+            result = client.generate(prompt="hello")
+
+        self.assertEqual(result, "hello")
+        mock_completion.assert_called_once()
+
+    def test_generate_malformed_sdk_response_raises_attribute_error(self):
+        client = LiteLLMClient(model_name="openai/gpt-4.1-mini")
+
+        with patch("hugegraph_llm.models.llms.litellm.completion", 
return_value=object()):
+            with self.assertRaises(AttributeError):
+                client.generate(prompt="hello")
+
     def test_budget_exceeded_error_is_not_retried(self):
         client = LiteLLMClient(model_name="openai/gpt-4.1-mini")
         error = BudgetExceededError(current_cost=2.0, max_budget=1.0)
diff --git a/hugegraph-llm/src/tests/models/llms/test_openai_client.py 
b/hugegraph-llm/src/tests/models/llms/test_openai_client.py
index 477e8fb3..cee6e9e0 100644
--- a/hugegraph-llm/src/tests/models/llms/test_openai_client.py
+++ b/hugegraph-llm/src/tests/models/llms/test_openai_client.py
@@ -113,6 +113,18 @@ class TestOpenAIClient(unittest.TestCase):
         with self.assertRaisesRegex(RuntimeError, "Empty choices in LLM 
response"):
             openai_client.generate(prompt="What is the capital of France?")
 
+    @patch("hugegraph_llm.models.llms.openai.OpenAI")
+    def test_generate_malformed_sdk_response_raises_attribute_error(self, 
mock_openai_class):
+        """Test malformed SDK responses surface the missing contract."""
+        mock_client = MagicMock()
+        mock_client.chat.completions.create.return_value = object()
+        mock_openai_class.return_value = mock_client
+
+        openai_client = OpenAIClient(model_name="gpt-3.5-turbo")
+
+        with self.assertRaises(AttributeError):
+            openai_client.generate(prompt="What is the capital of France?")
+
     @patch("hugegraph_llm.models.llms.openai.AsyncOpenAI")
     def test_agenerate(self, mock_async_openai_class):
         """Test agenerate method with mocked async OpenAI client."""
diff --git a/hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py 
b/hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py
index fe68428b..9a6e7659 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_gremlin_generate.py
@@ -25,6 +25,7 @@ import pytest
 
 from hugegraph_llm.models.llms.base import BaseLLM
 from hugegraph_llm.operators.llm_op.gremlin_generate import 
GremlinGenerateSynthesize
+from tests.fixtures.fake_llm import FakeLLM
 
 pytestmark = pytest.mark.contract
 
@@ -126,6 +127,30 @@ class TestGremlinGenerateSynthesize(unittest.TestCase):
         result = generator._extract_response("No gremlin code block here")
         self.assertEqual(result, "No gremlin code block here")
 
+    def test_extract_gremlin_from_explanation_plus_block(self):
+        """Test explanation text is stripped when a Gremlin block exists."""
+        generator = GremlinGenerateSynthesize(llm=self.mock_llm)
+        response = "Use this 
query:\n```gremlin\ng.V().hasLabel('person')\n```\nIt returns people."
+
+        self.assertEqual(generator._extract_response(response), 
"g.V().hasLabel('person')")
+
+    def test_extract_gremlin_uses_first_candidate_block(self):
+        """Test multiple candidate blocks resolve to the first public 
result."""
+        generator = GremlinGenerateSynthesize(llm=self.mock_llm)
+        response = 
"```gremlin\ng.V().limit(1)\n```\n```gremlin\ng.E().limit(1)\n```"
+
+        self.assertEqual(generator._extract_response(response), 
"g.V().limit(1)")
+
+    def test_run_with_fake_llm_empty_output_returns_empty_gremlin(self):
+        """Test empty LLM output is exposed as an empty Gremlin string."""
+        generator = GremlinGenerateSynthesize(llm=FakeLLM(["", ""]))
+
+        result = generator.run({"query": self.query})
+
+        self.assertEqual(result["result"], "")
+        self.assertEqual(result["raw_result"], "")
+        self.assertEqual(result["call_count"], 2)
+
     def test_format_examples(self):
         """Test the _format_examples method."""
         generator = GremlinGenerateSynthesize(llm=self.mock_llm)
diff --git a/hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py 
b/hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py
index d03c6242..6dbeb01e 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_keyword_extract.py
@@ -24,6 +24,7 @@ import pytest
 
 from hugegraph_llm.models.llms.base import BaseLLM
 from hugegraph_llm.operators.llm_op.keyword_extract import KeywordExtract
+from tests.fixtures.fake_llm import FakeLLM
 
 pytestmark = pytest.mark.contract
 
@@ -256,6 +257,32 @@ KEYWORDS: artificial intelligence:0.9, missing-score, bad 
score:not-a-number, gr
         self.assertNotIn("missing-score", keywords)
         self.assertNotIn("bad score", keywords)
 
+    def test_run_with_fake_llm_empty_output_returns_empty_keywords(self):
+        """Test empty provider output follows the public empty-list 
fallback."""
+        extractor = KeywordExtract(text=self.query, llm=FakeLLM([""]), 
max_keywords=5)
+
+        result = extractor.run({})
+
+        self.assertEqual(result["keywords"], [])
+        self.assertEqual(result["call_count"], 1)
+
+    def test_extract_keywords_from_response_deduplicates_by_last_score(self):
+        """Test duplicate keyword entries are normalized by dict semantics."""
+        response = "KEYWORDS: graph:0.3, vector:0.4, graph:0.9"
+
+        keywords = self.extractor._extract_keywords_from_response(response, 
lowercase=False, start_token="KEYWORDS:")
+
+        self.assertEqual(keywords["graph"], 0.9)
+        self.assertEqual(keywords["vector"], 0.4)
+
+    def 
test_extract_keywords_from_non_list_provider_text_returns_empty_mapping(self):
+        """Test non keyword-list provider text does not leak as a keyword."""
+        response = "The answer is probably graph databases."
+
+        keywords = self.extractor._extract_keywords_from_response(response, 
lowercase=False, start_token="KEYWORDS:")
+
+        self.assertEqual(keywords, {})
+
     def test_extract_keywords_from_response_without_start_token(self):
         """Test _extract_keywords_from_response method without start token."""
         response = "artificial intelligence:0.9, machine learning:0.8, neural 
networks:0.7"
diff --git 
a/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py 
b/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
index 7bef62d5..3b4bd25e 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
@@ -30,6 +30,7 @@ from hugegraph_llm.operators.llm_op.property_graph_extract 
import (
     generate_extract_property_graph_prompt,
     split_text,
 )
+from tests.fixtures.fake_llm import FakeLLM
 
 pytestmark = pytest.mark.contract
 
@@ -393,6 +394,127 @@ Hope this helps."""
 
         self.assertEqual(result, [])
 
+    def test_property_graph_extract_strips_fenced_json_with_fake_llm(self):
+        """Test fake LLM fixture drives fenced JSON parser behavior."""
+        llm = FakeLLM(['```json\n{"vertices": [], "edges": []}\n```'])
+        extractor = PropertyGraphExtract(llm=llm)
+
+        result = extractor._extract_and_filter_label({"vertexlabels": [], 
"edgelabels": []}, llm.generate())
+
+        self.assertEqual(result, [])
+        self.assertEqual(len(llm.calls), 1)
+
+    def test_extract_and_filter_label_resolves_numeric_vertex_ids(self):
+        """Test numeric LLM vertex ids can be resolved before commit."""
+        extractor = PropertyGraphExtract(llm=self.mock_llm)
+        text = """{
+            "vertices": [
+            {
+                "id": 101,
+                "label": "person",
+                "properties": {
+                    "name": "Tom Hanks"
+                }
+            },
+            {
+                "id": 202,
+                "label": "movie",
+                "properties": {
+                    "title": "Forrest Gump"
+                }
+            }
+            ],
+            "edges": [
+            {
+                "label": "acted_in",
+                "outV": 101,
+                "outVLabel": "person",
+                "inV": 202,
+                "inVLabel": "movie",
+                "properties": {
+                    "role": "Forrest Gump"
+                }
+            }
+            ]
+        }"""
+
+        result = extractor._extract_and_filter_label(self.schema, text)
+
+        self.assertEqual(result[0]["id"], "1:Tom Hanks")
+        self.assertEqual(result[1]["id"], "2:Forrest Gump")
+        self.assertEqual(result[2]["outV"], "1:Tom Hanks")
+        self.assertEqual(result[2]["inV"], "2:Forrest Gump")
+
+    def 
test_extract_and_filter_label_preserves_duplicate_items_deterministically(self):
+        """Test duplicate vertices and edges are parsed in stable order."""
+        extractor = PropertyGraphExtract(llm=self.mock_llm)
+        text = """{
+            "vertices": [
+            {
+                "label": "person",
+                "properties": {
+                    "name": "Tom Hanks"
+                }
+            },
+            {
+                "label": "person",
+                "properties": {
+                    "name": "Tom Hanks"
+                }
+            },
+            {
+                "label": "movie",
+                "properties": {
+                    "title": "Forrest Gump"
+                }
+            }
+            ],
+            "edges": [
+            {
+                "label": "acted_in",
+                "properties": {
+                    "role": "Forrest Gump"
+                },
+                "source": {
+                    "label": "person",
+                    "properties": {
+                        "name": "Tom Hanks"
+                    }
+                },
+                "target": {
+                    "label": "movie",
+                    "properties": {
+                        "title": "Forrest Gump"
+                    }
+                }
+            },
+            {
+                "label": "acted_in",
+                "properties": {
+                    "role": "Forrest Gump"
+                },
+                "source": {
+                    "label": "person",
+                    "properties": {
+                        "name": "Tom Hanks"
+                    }
+                },
+                "target": {
+                    "label": "movie",
+                    "properties": {
+                        "title": "Forrest Gump"
+                    }
+                }
+            }
+            ]
+        }"""
+
+        result = extractor._extract_and_filter_label(self.schema, text)
+
+        self.assertEqual([item["type"] for item in result], ["vertex", 
"vertex", "vertex", "edge", "edge"])
+        self.assertEqual(result[0]["id"], "1:Tom Hanks")
+        self.assertEqual(result[1]["id"], "1:Tom Hanks")
+
     def test_extract_and_filter_label_infers_type_from_grouped_arrays(self):
         """Infer item type from vertices/edges containers when LLM omits it."""
         extractor = PropertyGraphExtract(llm=self.mock_llm)


Reply via email to