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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3872443b refactor: enhance the tests frame & quality in core modules 
(#357)
3872443b is described below

commit 3872443b073b1332bd8089c6acda8eda857028d8
Author: imbajin <[email protected]>
AuthorDate: Tue Jun 2 20:37:34 2026 +0800

    refactor: enhance the tests frame & quality in core modules (#357)
    
    - define the long-task quality program scope and guardrails
    - split execution into client-first and llm-focused goals
    - specify test layers, CI gate evolution, and coverage ratchet
    - document checkpoints, deferred refactors, and PR collision rules
---
 .github/workflows/auto-pr-comment.yml              |   1 +
 .github/workflows/hugegraph-llm.yml                | 171 +++++++++-
 .github/workflows/hugegraph-python-client.yml      |  95 +++++-
 docs/quality/coverage-ratchet.md                   |  66 ++++
 docs/quality/hugegraph-integration.md              |  20 ++
 docs/quality/test-taxonomy.md                      |  32 ++
 hugegraph-llm/src/hugegraph_llm/api/rag_api.py     |   4 +-
 .../src/hugegraph_llm/config/models/base_config.py |  28 +-
 .../config/models/base_prompt_config.py            |  47 ++-
 .../hugegraph_llm/demo/rag_demo/configs_block.py   |   3 +-
 .../operators/hugegraph_op/commit_to_hugegraph.py  |  16 +-
 hugegraph-llm/src/tests/api/test_rag_api.py        | 115 +++++++
 hugegraph-llm/src/tests/config/test_config.py      |  63 ++++
 .../src/tests/config/test_prompt_config.py         |   4 +
 hugegraph-llm/src/tests/conftest.py                |  62 +++-
 .../data/quality_program/graphrag_documents.json   |   4 +
 .../data/quality_program/kg_graph_output.json      |  14 +
 .../src/tests/data/quality_program/kg_text.txt     |   1 +
 .../data/quality_program/text2gremlin_schema.json  |   9 +
 hugegraph-llm/src/tests/document/test_document.py  |   4 +
 .../src/tests/document/test_document_splitter.py   |   4 +
 .../src/tests/document/test_text_loader.py         |   4 +
 .../test_config.py => fixtures/__init__.py}        |  13 -
 .../test_config.py => fixtures/fake_llm.py}        |  20 +-
 .../src/tests/fixtures/hugegraph_service.py        |  74 +++++
 .../tests/integration/test_core_graphrag_smoke.py  |  89 +++++
 .../src/tests/integration/test_core_kg_smoke.py    | 108 ++++++
 .../integration/test_core_text2gremlin_smoke.py    |  68 ++++
 .../tests/integration/test_graph_rag_pipeline.py   |   4 +
 .../tests/integration/test_hugegraph_boundary.py   | 187 +++++++++++
 .../src/tests/integration/test_kg_construction.py  |   4 +
 .../src/tests/integration/test_rag_pipeline.py     |   4 +
 .../src/tests/middleware/test_middleware.py        |   3 +
 .../models/embeddings/test_ollama_embedding.py     |  30 ++
 .../src/tests/models/llms/test_litellm_client.py   |  24 +-
 .../src/tests/models/llms/test_openai_client.py    |  16 +
 .../hugegraph_op/test_commit_to_hugegraph.py       | 254 +-------------
 .../test_commit_to_hugegraph_load_into_graph.py    | 366 +++++++++++++++++++++
 .../hugegraph_op/test_fetch_graph_data.py          |   4 +
 .../operators/hugegraph_op/test_schema_manager.py  |   4 +
 .../operators/llm_op/test_disambiguate_data.py     |   4 +
 .../operators/llm_op/test_gremlin_generate.py      |  29 ++
 .../tests/operators/llm_op/test_info_extract.py    |   4 +
 .../tests/operators/llm_op/test_keyword_extract.py |  31 ++
 .../llm_op/test_property_graph_extract.py          | 126 +++++++
 .../src/pyhugegraph/utils/util.py                  |  11 +-
 hugegraph-python-client/src/tests/api/test_auth.py |   3 +
 .../src/tests/api/test_auth_routing.py             |   2 +
 .../src/tests/api/test_graph.py                    |  15 +
 .../src/tests/api/test_graphs.py                   |   4 +
 .../src/tests/api/test_gremlin.py                  |  10 +
 .../src/tests/api/test_metric.py                   |   4 +
 .../src/tests/api/test_response_validation.py      |  41 +++
 .../src/tests/api/test_schema.py                   |  42 +++
 hugegraph-python-client/src/tests/api/test_task.py |   3 +
 .../src/tests/api/test_traverser.py                |   4 +
 .../src/tests/api/test_variable.py                 |   2 +
 .../src/tests/api/test_version.py                  |   4 +
 hugegraph-python-client/src/tests/client_utils.py  |  16 +-
 .../src/tests/conftest.py                          |  25 +-
 .../src/tests/fixtures/__init__.py                 |  13 -
 .../src/tests/fixtures/hugegraph_service.py        |  75 +++++
 pyproject.toml                                     |  12 +
 63 files changed, 2160 insertions(+), 359 deletions(-)

diff --git a/.github/workflows/auto-pr-comment.yml 
b/.github/workflows/auto-pr-comment.yml
index 6a585355..f6561fed 100644
--- a/.github/workflows/auto-pr-comment.yml
+++ b/.github/workflows/auto-pr-comment.yml
@@ -28,6 +28,7 @@ jobs:
       pull-requests: write
     steps:
       - name: Add review comment
+        # This workflow is only enabled in fork repositories to request AI 
review.
         uses: peter-evans/create-or-update-comment@v4
         with:
           issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/hugegraph-llm.yml 
b/.github/workflows/hugegraph-llm.yml
index b813483c..f14bd567 100644
--- a/.github/workflows/hugegraph-llm.yml
+++ b/.github/workflows/hugegraph-llm.yml
@@ -26,8 +26,14 @@ on:
       - 'release-*'
   pull_request:
 
+permissions:
+  contents: read
+
+env:
+  UV_VERSION: "0.9.22"
+
 jobs:
-  build:
+  llm-unit-contract:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
@@ -35,25 +41,23 @@ jobs:
         python-version: ["3.10", "3.11"]
 
     steps:
-    - name: Prepare HugeGraph Server Environment
-      run: |
-        docker run -d --name=graph -p 8080:8080 -e PASSWORD=admin 
hugegraph/hugegraph:1.5.0
-        sleep 10
-
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
         python-version: ${{ matrix.python-version }}
 
     - name: Install uv
       run: |
-        curl -LsSf https://astral.sh/uv/install.sh | sh
-        echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+        curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh"; | sh
+        echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+        "$HOME/.local/bin/uv" --version
 
     - name: Cache dependencies
-      uses: actions/cache@v4
+      uses: actions/cache@v5
       with:
         path: |
           ~/.cache/uv
@@ -68,15 +72,148 @@ jobs:
         uv run python -c "import nltk; nltk.download('stopwords'); 
nltk.download('punkt')"
 
     - name: Run unit tests
-      working-directory: hugegraph-llm
       env:
         SKIP_EXTERNAL_SERVICES: true
       run: |
-        uv run pytest src/tests/config/ src/tests/document/ 
src/tests/middleware/ src/tests/operators/ src/tests/models/ src/tests/indices/ 
src/tests/test_utils.py -v --tb=short
+        uv run pytest hugegraph-llm/src/tests -m "not integration and not 
hugegraph and not smoke and not external" --cov=hugegraph_llm 
--cov-fail-under=34 --cov-report=term --cov-report=xml:llm-unit-contract.xml -v 
--tb=short --durations=20
+
+    - name: Upload coverage artifact
+      uses: actions/upload-artifact@v7
+      with:
+        name: llm-unit-contract-coverage-${{ matrix.python-version }}
+        path: llm-unit-contract.xml
+        if-no-files-found: error
+
+  llm-hugegraph-boundary:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11"]
+
+    services:
+      hugegraph:
+        image: hugegraph/hugegraph:1.7.0
+        env:
+          PASSWORD: admin
+        options: --health-cmd="curl -f http://localhost:8080/versions || exit 
1" --health-interval=10s --health-timeout=5s --health-retries=8
+        ports:
+          - 8080:8080
+
+    steps:
+    - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
-    - name: Run integration tests
-      working-directory: hugegraph-llm
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install uv
+      run: |
+        curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh"; | sh
+        echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+        "$HOME/.local/bin/uv" --version
+
+    - name: Cache dependencies
+      uses: actions/cache@v5
+      with:
+        path: |
+          ~/.cache/uv
+          ~/nltk_data
+        key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ 
hashFiles('**/pyproject.toml', 'uv.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-uv-${{ matrix.python-version }}-
+
+    - name: Install dependencies
+      run: |
+        uv sync --extra llm --extra dev
+        uv run python -c "import nltk; nltk.download('stopwords'); 
nltk.download('punkt')"
+
+    - name: Run HugeGraph boundary tests
       env:
-        SKIP_EXTERNAL_SERVICES: true
+        HUGEGRAPH_REQUIRED: true
+        HUGEGRAPH_URL: http://127.0.0.1:8080
+        HUGEGRAPH_GRAPH: hugegraph
+        HUGEGRAPH_USER: admin
+        HUGEGRAPH_PASSWORD: admin
+        SKIP_EXTERNAL_SERVICES: false
+      run: |
+        uv run pytest hugegraph-llm/src/tests -m "integration and hugegraph" 
-v --tb=short
+
+    - name: Dump HugeGraph diagnostics
+      if: failure()
+      run: |
+        docker ps -a
+        container_id=$(docker ps -aq --filter 
ancestor=hugegraph/hugegraph:1.7.0 | head -n 1)
+        if [ -n "$container_id" ]; then
+          docker logs "$container_id"
+        fi
+
+  llm-core-smoke:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11"]
+
+    services:
+      hugegraph:
+        image: hugegraph/hugegraph:1.7.0
+        env:
+          PASSWORD: admin
+        options: --health-cmd="curl -f http://localhost:8080/versions || exit 
1" --health-interval=10s --health-timeout=5s --health-retries=8
+        ports:
+          - 8080:8080
+
+    steps:
+    - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install uv
+      run: |
+        curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh"; | sh
+        echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+        "$HOME/.local/bin/uv" --version
+
+    - name: Cache dependencies
+      uses: actions/cache@v5
+      with:
+        path: |
+          ~/.cache/uv
+          ~/nltk_data
+        key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ 
hashFiles('**/pyproject.toml', 'uv.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-uv-${{ matrix.python-version }}-
+
+    - name: Install dependencies
+      run: |
+        uv sync --extra llm --extra dev
+        uv run python -c "import nltk; nltk.download('stopwords'); 
nltk.download('punkt')"
+
+    - name: Run deterministic core smoke tests
+      env:
+        HUGEGRAPH_REQUIRED: true
+        HUGEGRAPH_URL: http://127.0.0.1:8080
+        HUGEGRAPH_GRAPH: hugegraph
+        HUGEGRAPH_USER: admin
+        HUGEGRAPH_PASSWORD: admin
+        SKIP_EXTERNAL_SERVICES: false
+      run: |
+        uv run pytest hugegraph-llm/src/tests/integration -m "smoke" -v 
--tb=short
+
+    - name: Dump HugeGraph diagnostics
+      if: failure()
       run: |
-        uv run pytest src/tests/integration/test_graph_rag_pipeline.py 
src/tests/integration/test_kg_construction.py 
src/tests/integration/test_rag_pipeline.py -v --tb=short
+        docker ps -a
+        container_id=$(docker ps -aq --filter 
ancestor=hugegraph/hugegraph:1.7.0 | head -n 1)
+        if [ -n "$container_id" ]; then
+          docker logs "$container_id"
+        fi
diff --git a/.github/workflows/hugegraph-python-client.yml 
b/.github/workflows/hugegraph-python-client.yml
index 5dcf27c1..ab8915f3 100644
--- a/.github/workflows/hugegraph-python-client.yml
+++ b/.github/workflows/hugegraph-python-client.yml
@@ -7,39 +7,96 @@ on:
       - 'release-*'
   pull_request:
 
+permissions:
+  contents: read
+
+env:
+  UV_VERSION: "0.9.22"
+
 jobs:
-  build:
+  client-unit-contract:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11"]
+
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v6
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install uv
+        run: |
+          curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh"; | sh
+          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+          "$HOME/.local/bin/uv" --version
+
+      - name: Cache dependencies
+        id: cache-deps
+        uses: actions/cache@v5
+        with:
+          path: |
+            ~/.cache/uv
+          key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ 
hashFiles('**/pyproject.toml') }}
+          restore-keys: |
+            ${{ runner.os }}-uv-${{ matrix.python-version }}-
+
+      - name: Install dependencies with uv sync
+        run: |
+          uv sync --extra python-client --extra dev
+
+      - name: Run unit and contract tests
+        run: |
+          uv run pytest hugegraph-python-client/src/tests -m "unit or 
contract" --cov=pyhugegraph --cov-fail-under=45 --cov-report=term 
--cov-report=xml:client-unit-contract.xml -v --tb=short --durations=20
+
+      - name: Upload coverage artifact
+        uses: actions/upload-artifact@v7
+        with:
+          name: client-unit-contract-coverage-${{ matrix.python-version }}
+          path: client-unit-contract.xml
+          if-no-files-found: error
+
+  client-hugegraph-integration:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.10", "3.11", "3.12"]
+        python-version: ["3.10", "3.11"]
 
     services:
       hugegraph:
         image: hugegraph/hugegraph:1.7.0
         env:
           PASSWORD: admin
-        options: --health-cmd="curl -f http://localhost:8080/versions || exit 
1" --health-interval=10s --health-timeout=5s --health-retries=5
+        options: --health-cmd="curl -f http://localhost:8080/versions || exit 
1" --health-interval=10s --health-timeout=5s --health-retries=8
         ports:
           - 8080:8080
 
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
           python-version: ${{ matrix.python-version }}
 
       - name: Install uv
         run: |
-          curl -LsSf https://astral.sh/uv/install.sh | sh
-          echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+          curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh"; | sh
+          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+          "$HOME/.local/bin/uv" --version
 
       - name: Cache dependencies
         id: cache-deps
-        uses: actions/cache@v4
+        uses: actions/cache@v5
         with:
           path: |
             ~/.cache/uv
@@ -51,13 +108,21 @@ jobs:
         run: |
           uv sync --extra python-client --extra dev
 
-      - name: Test example
+      - name: Run HugeGraph integration tests
+        env:
+          HUGEGRAPH_REQUIRED: true
+          HUGEGRAPH_URL: http://127.0.0.1:8080
+          HUGEGRAPH_GRAPH: hugegraph
+          HUGEGRAPH_USER: admin
+          HUGEGRAPH_PASSWORD: admin
         run: |
-          ls -al
-          export PYTHONPATH=$(pwd)/hugegraph-python-client/src && echo 
${PYTHONPATH}
-          uv run python 
hugegraph-python-client/src/pyhugegraph/example/hugegraph_example.py
+          uv run pytest hugegraph-python-client/src/tests -m "integration and 
hugegraph" -v --tb=short
 
-      - name: Test with pytest
+      - name: Dump HugeGraph diagnostics
+        if: failure()
         run: |
-          uv run pytest
-        working-directory: hugegraph-python-client
+          docker ps -a
+          container_id=$(docker ps -aq --filter 
ancestor=hugegraph/hugegraph:1.7.0 | head -n 1)
+          if [ -n "$container_id" ]; then
+            docker logs "$container_id"
+          fi
diff --git a/docs/quality/coverage-ratchet.md b/docs/quality/coverage-ratchet.md
new file mode 100644
index 00000000..082c4b3e
--- /dev/null
+++ b/docs/quality/coverage-ratchet.md
@@ -0,0 +1,66 @@
+# Coverage Ratchet
+
+## Principles
+
+- Start with local areas, not a full-repo threshold.
+- New production logic requires tests.
+- Bug fixes require regression tests.
+- HugeGraph boundaries need Layer B or Layer C evidence.
+- Thresholds may start low but must not decrease.
+- External provider, vector DB, and UI credential paths stay outside default 
PR ratchets.
+
+## Initial Ratchet Areas
+
+- `pyhugegraph`
+- `hugegraph_llm.operators.hugegraph_op`
+- `hugegraph_llm.operators.llm_op`
+- `hugegraph_llm.api`
+- `hugegraph_llm.api.models`
+
+## Baseline
+
+Current combined baseline:
+
+| Scope | Covered lines | Statements | Missing lines | Coverage |
+|---|---:|---:|---:|---:|
+| `pyhugegraph` + `hugegraph_llm` unit/contract | 3115 | 7975 | 4860 | 39% |
+
+The original combined command hits a pytest import-path collision because both 
workspace packages define a `tests.conftest` module. Use 
`--import-mode=importlib` for combined workspace baselines until the test 
package layout is normalized.
+
+## CI Gates
+
+The default unit/contract jobs enforce module-level floors from the initial 
local baseline runs:
+
+| Workflow | Scope | Gate |
+|---|---|---:|
+| `.github/workflows/hugegraph-python-client.yml` | `pyhugegraph` 
unit/contract | `--cov-fail-under=45` |
+| `.github/workflows/hugegraph-llm.yml` | `hugegraph_llm` unit/contract | 
`--cov-fail-under=34` |
+
+These gates are intentionally baseline-level floors. Raise them only after the 
corresponding local ratchet areas gain meaningful tests.
+
+## Local Commands
+
+```bash
+uv run pytest hugegraph-python-client/src/tests -m "unit or contract" 
--cov=pyhugegraph --cov-report=term
+uv run pytest hugegraph-llm/src/tests/operators/hugegraph_op -m "unit or 
contract" --cov=hugegraph_llm.operators.hugegraph_op --cov-report=term
+uv run pytest hugegraph-llm/src/tests/operators/llm_op -m "unit or contract" 
--cov=hugegraph_llm.operators.llm_op --cov-report=term
+uv run pytest hugegraph-llm/src/tests/api -m "unit or contract" 
--cov=hugegraph_llm.api --cov-report=term
+```
+
+Combined workspace baseline command:
+
+```bash
+uv run pytest --import-mode=importlib hugegraph-python-client/src/tests 
hugegraph-llm/src/tests \
+  -m "unit or contract" \
+  --cov=pyhugegraph \
+  --cov=hugegraph_llm \
+  --cov-report=term \
+  --cov-report=json:combined-baseline.json
+```
+
+## Ratchet Rules
+
+- Do not lower an existing local threshold for a touched ratchet area.
+- For production bug fixes, add or update a regression test in the nearest 
unit, contract, integration, or smoke layer.
+- For HugeGraph service-boundary changes, include a selected Layer B or Layer 
C command with `HUGEGRAPH_REQUIRED=true`.
+- For new external-provider behavior, keep deterministic contract tests in 
default gates and reserve live-provider coverage for Layer D.
diff --git a/docs/quality/hugegraph-integration.md 
b/docs/quality/hugegraph-integration.md
new file mode 100644
index 00000000..0edbc486
--- /dev/null
+++ b/docs/quality/hugegraph-integration.md
@@ -0,0 +1,20 @@
+# HugeGraph Integration Test Service
+
+Default image: `hugegraph/hugegraph:1.7.0`
+
+## Environment
+
+```text
+HUGEGRAPH_URL=http://127.0.0.1:8080
+HUGEGRAPH_GRAPH=hugegraph
+HUGEGRAPH_USER=admin
+HUGEGRAPH_PASSWORD=admin
+HUGEGRAPH_GRAPHSPACE=
+HUGEGRAPH_REQUIRED=true|false
+```
+
+## Semantics
+
+- If `HUGEGRAPH_REQUIRED=true`, selected integration tests fail when the 
service is unavailable.
+- If `HUGEGRAPH_REQUIRED=false`, local integration tests may skip when no 
service is present.
+- Default unit/contract tests must not require Docker.
diff --git a/docs/quality/test-taxonomy.md b/docs/quality/test-taxonomy.md
new file mode 100644
index 00000000..a93ecb42
--- /dev/null
+++ b/docs/quality/test-taxonomy.md
@@ -0,0 +1,32 @@
+# HugeGraph AI Test Taxonomy
+
+## Layer A: Unit / Pure Contract
+
+- Markers: `unit` or `contract`.
+- No Docker, network, real HugeGraph, real LLM provider, embedding provider, 
reranker provider, vector DB, or UI service.
+- Use fakes, fixtures, monkeypatches, and public APIs.
+
+## Layer B: HugeGraph Server Contract
+
+- Markers: `integration` and `hugegraph`.
+- Requires HugeGraph Server, default `hugegraph/hugegraph:1.7.0`.
+- If selected with `HUGEGRAPH_REQUIRED=true`, service connection failures fail.
+- Must import production code and validate real server behavior.
+
+## Layer C: Core Pipeline Smoke
+
+- Marker: `smoke`; also use `integration` and `hugegraph` when real HugeGraph 
is required.
+- Uses production flow/node/operator code.
+- Uses fake LLM and deterministic embeddings/vector fixtures.
+- Does not define local replacement pipeline implementations inside tests.
+
+## Layer D: External Provider / Optional E2E
+
+- Markers: `external` and usually `slow`.
+- May require real provider credentials or non-HugeGraph services.
+- Excluded from default PR gates.
+
+## Required Skip Semantics
+
+Do not silently skip selected Layer B tests. Prefer not selecting integration 
tests locally.
+If `HUGEGRAPH_REQUIRED=true`, unavailable HugeGraph is a failure.
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index 76217251..4f5a80c3 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -161,7 +161,9 @@ 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
+        llm_settings.extract_llm_type = req.llm_type
+        llm_settings.text2gql_llm_type = req.llm_type
 
         if req.llm_type == "openai":
             res = apply_llm_conf(
diff --git a/hugegraph-llm/src/hugegraph_llm/config/models/base_config.py 
b/hugegraph-llm/src/hugegraph_llm/config/models/base_config.py
index 5fec3a77..0e5ca609 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/models/base_config.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/models/base_config.py
@@ -17,6 +17,7 @@
 
 
 import os
+from pathlib import Path
 
 from dotenv import dotenv_values, set_key
 from pydantic_settings import BaseSettings
@@ -24,7 +25,32 @@ from pydantic_settings import BaseSettings
 from hugegraph_llm.utils.log import log
 
 dir_name = os.path.dirname
-env_path = os.path.join(os.getcwd(), ".env")  # Load .env from the current 
working directory
+ENV_PATH_ENV_VAR = "HUGEGRAPH_LLM_ENV_PATH"
+
+
+def _source_project_root() -> Path | None:
+    package_root = Path(__file__).resolve().parents[2]
+    if package_root.parent.name != "src":
+        return None
+    project_root = package_root.parent.parent
+    if (project_root / "pyproject.toml").exists():
+        return project_root
+    return None
+
+
+def resolve_env_path() -> str:
+    explicit_env_path = os.getenv(ENV_PATH_ENV_VAR)
+    if explicit_env_path:
+        return str(Path(explicit_env_path).expanduser())
+
+    project_root = _source_project_root()
+    if project_root is not None:
+        return str(project_root / ".env")
+
+    return str(Path.cwd() / ".env")
+
+
+env_path = resolve_env_path()
 
 
 class BaseConfig(BaseSettings):
diff --git 
a/hugegraph-llm/src/hugegraph_llm/config/models/base_prompt_config.py 
b/hugegraph-llm/src/hugegraph_llm/config/models/base_prompt_config.py
index 57d8ba63..f1e0c6c1 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/models/base_prompt_config.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/models/base_prompt_config.py
@@ -16,17 +16,45 @@
 # under the License.
 
 import os
-import sys
 from pathlib import Path
 
 import yaml
 
-from hugegraph_llm.utils.anchor import get_project_root
 from hugegraph_llm.utils.log import log
 
 dir_name = os.path.dirname
 F_NAME = "config_prompt.yaml"
-yaml_file_path = os.path.join(os.getcwd(), "src/hugegraph_llm/resources/demo", 
F_NAME)
+PROMPT_CONFIG_PATH_ENV_VAR = "HUGEGRAPH_LLM_PROMPT_CONFIG_PATH"
+
+
+def _source_prompt_yaml_path() -> Path | None:
+    package_root = Path(__file__).resolve().parents[2]
+    if package_root.parent.name != "src":
+        return None
+    project_root = package_root.parent.parent
+    if (project_root / "pyproject.toml").exists():
+        return package_root / "resources" / "demo" / F_NAME
+    return None
+
+
+def _user_prompt_yaml_path() -> Path:
+    config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / 
".config")).expanduser()
+    return config_home / "hugegraph-llm" / F_NAME
+
+
+def resolve_prompt_yaml_path() -> str:
+    explicit_prompt_path = os.getenv(PROMPT_CONFIG_PATH_ENV_VAR)
+    if explicit_prompt_path:
+        return str(Path(explicit_prompt_path).expanduser())
+
+    source_prompt_path = _source_prompt_yaml_path()
+    if source_prompt_path is not None:
+        return str(source_prompt_path)
+
+    return str(_user_prompt_yaml_path())
+
+
+yaml_file_path = resolve_prompt_yaml_path()
 
 
 class LiteralStr(str):
@@ -54,18 +82,6 @@ class BasePromptConfig:
     generate_extract_prompt_template: str = ""
 
     def ensure_yaml_file_exists(self):
-        current_dir = Path.cwd().resolve()
-        project_root = get_project_root()
-        if current_dir == project_root:
-            log.info("Current working directory is the project root, 
proceeding to run the app.")
-        else:
-            error_msg = (
-                f"Current working directory is not the project root. "
-                f"Please run this script from the project root directory: 
{project_root}\n"
-                f"Current directory: {current_dir}"
-            )
-            log.error(error_msg)
-            sys.exit(1)
         if os.path.exists(yaml_file_path):
             log.info("Loading prompt file '%s' successfully.", F_NAME)
             with open(yaml_file_path, "r", encoding="utf-8") as file:
@@ -123,6 +139,7 @@ class BasePromptConfig:
             "_language_generated": 
str(self.llm_settings.language).lower().strip(),
             "generate_extract_prompt_template": 
to_literal(self.generate_extract_prompt_template),
         }
+        Path(yaml_file_path).parent.mkdir(parents=True, exist_ok=True)
         with open(yaml_file_path, "w", encoding="utf-8") as file:
             yaml.dump(data, file, allow_unicode=True, sort_keys=False, 
default_flow_style=False)
 
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/configs_block.py 
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/configs_block.py
index 72c1eec0..0b51aa6c 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/configs_block.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/configs_block.py
@@ -16,7 +16,6 @@
 # under the License.
 
 import json
-import os
 from functools import partial
 from typing import Optional
 
@@ -26,6 +25,7 @@ from dotenv import dotenv_values
 from requests.auth import HTTPBasicAuth
 
 from hugegraph_llm.config import huge_settings, index_settings, llm_settings
+from hugegraph_llm.config.models.base_config import env_path
 from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
 from hugegraph_llm.models.llms.litellm import LiteLLMClient
 from hugegraph_llm.utils.log import log
@@ -429,7 +429,6 @@ def create_configs_block() -> list:
                 llm_config_button = gr.Button("Apply configuration")
                 llm_config_button.click(apply_llm_config_with_chat_op, 
inputs=llm_config_input)
                 # Determine whether there are Settings in the.env file
-                env_path = os.path.join(os.getcwd(), ".env")  # Load .env from 
the current working directory
                 env_vars = dotenv_values(env_path)
                 api_extract_key = env_vars.get("OPENAI_EXTRACT_API_KEY")
                 api_text2sql_key = env_vars.get("OPENAI_TEXT2GQL_API_KEY")
diff --git 
a/hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py 
b/hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
index daade304..14bb3865 100644
--- 
a/hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
+++ 
b/hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
@@ -147,20 +147,26 @@ class Commit2Graph:
                 continue
 
             # TODO: we could try batch add vertices first, setback to 
single-mode if failed
-            original_id = vertex.get("id")
-            if vertex_label.get("id_strategy") == "CUSTOMIZE_STRING" and 
original_id:
+            explicit_id = vertex.get("id")
+            mapping_id = explicit_id
+            if not mapping_id and primary_keys:
+                mapping_id = 
f"{input_label}:{'!'.join(str(input_properties[pk]) for pk in primary_keys)}"
+
+            if vertex_label.get("id_strategy") == "CUSTOMIZE_STRING" and 
explicit_id:
                 result = self._handle_graph_creation(
                     self.client.graph().addVertex,
                     input_label,
                     input_properties,
-                    id=original_id,
+                    id=explicit_id,
                 )
             else:
                 result = 
self._handle_graph_creation(self.client.graph().addVertex, input_label, 
input_properties)
+            if result is None:
+                raise ValueError(f"Failed to create vertex '{input_label}' 
with properties {input_properties}")
             vid = result.id
             vertex["id"] = vid
-            if original_id:
-                vid_mapping[original_id] = vid
+            if mapping_id:
+                vid_mapping[mapping_id] = vid
 
         for edge in edges:
             start = vid_mapping.get(edge.get("outV"), edge.get("outV"))
diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py 
b/hugegraph-llm/src/tests/api/test_rag_api.py
index 55fbd679..0bed3174 100644
--- a/hugegraph-llm/src/tests/api/test_rag_api.py
+++ b/hugegraph-llm/src/tests/api/test_rag_api.py
@@ -17,10 +17,32 @@
 
 from unittest.mock import Mock
 
+import pytest
 from fastapi import APIRouter, FastAPI, status
 from fastapi.testclient import TestClient
 
 from hugegraph_llm.api.rag_api import rag_http_api
+from hugegraph_llm.config import llm_settings
+
+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():
@@ -60,3 +82,96 @@ 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(monkeypatch):
+    monkeypatch.setattr(llm_settings, "chat_llm_type", "ollama/local")
+    monkeypatch.setattr(llm_settings, "extract_llm_type", "ollama/local")
+    monkeypatch.setattr(llm_settings, "text2gql_llm_type", "ollama/local")
+    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 llm_settings.chat_llm_type == "openai"
+    assert llm_settings.extract_llm_type == "openai"
+    assert llm_settings.text2gql_llm_type == "openai"
+    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/config/test_config.py 
b/hugegraph-llm/src/tests/config/test_config.py
index 63dc6cb9..9e9b9fc6 100644
--- a/hugegraph-llm/src/tests/config/test_config.py
+++ b/hugegraph-llm/src/tests/config/test_config.py
@@ -17,6 +17,15 @@
 
 
 import unittest
+from os import environ
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import pytest
+
+pytestmark = pytest.mark.unit
 
 
 class TestConfig(unittest.TestCase):
@@ -27,3 +36,57 @@ class TestConfig(unittest.TestCase):
 
         nltk.data.path.append(resource_path)
         nltk.data.find("corpora/stopwords")
+
+    def test_prompt_yaml_path_is_project_root_independent(self):
+        from hugegraph_llm.config.models import base_prompt_config
+
+        expected = Path(__file__).resolve().parents[2] / "hugegraph_llm" / 
"resources" / "demo" / "config_prompt.yaml"
+        self.assertEqual(Path(base_prompt_config.yaml_file_path), expected)
+
+    def test_prompt_yaml_path_prefers_explicit_override(self):
+        from hugegraph_llm.config.models import base_prompt_config
+
+        custom_prompt = Path("/tmp/hugegraph-llm-prompt-test.yaml")
+        with patch.dict(environ, 
{base_prompt_config.PROMPT_CONFIG_PATH_ENV_VAR: str(custom_prompt)}):
+            
self.assertEqual(Path(base_prompt_config.resolve_prompt_yaml_path()), 
custom_prompt)
+
+    def test_prompt_yaml_path_uses_user_config_outside_source_tree(self):
+        from hugegraph_llm.config.models import base_prompt_config
+
+        with TemporaryDirectory() as config_home:
+            with patch.dict(environ, {"XDG_CONFIG_HOME": config_home}, 
clear=False):
+                with patch.object(base_prompt_config, 
"_source_prompt_yaml_path", return_value=None):
+                    expected = Path(config_home) / "hugegraph-llm" / 
"config_prompt.yaml"
+                    
self.assertEqual(Path(base_prompt_config.resolve_prompt_yaml_path()), expected)
+
+    def test_prompt_save_creates_mutable_config_parent_directory(self):
+        from hugegraph_llm.config.models import base_prompt_config
+        from hugegraph_llm.config.prompt_config import PromptConfig
+
+        with TemporaryDirectory() as temp_dir:
+            prompt_path = Path(temp_dir) / "nested" / "config_prompt.yaml"
+            prompt = PromptConfig(SimpleNamespace(language="en"))
+
+            with patch.object(base_prompt_config, "yaml_file_path", 
str(prompt_path)):
+                prompt.save_to_yaml()
+
+            self.assertTrue(prompt_path.exists())
+
+    def test_env_path_is_project_root_independent(self):
+        from hugegraph_llm.config.models import base_config
+
+        expected = Path(__file__).resolve().parents[3] / ".env"
+        self.assertEqual(Path(base_config.env_path), expected)
+
+    def test_env_path_prefers_explicit_override(self):
+        from hugegraph_llm.config.models import base_config
+
+        custom_env = Path("/tmp/hugegraph-llm-test.env")
+        with patch.dict(environ, {base_config.ENV_PATH_ENV_VAR: 
str(custom_env)}):
+            self.assertEqual(Path(base_config.resolve_env_path()), custom_env)
+
+    def test_demo_config_block_uses_shared_env_path(self):
+        from hugegraph_llm.config.models import base_config
+        from hugegraph_llm.demo.rag_demo import configs_block
+
+        self.assertEqual(configs_block.env_path, base_config.env_path)
diff --git a/hugegraph-llm/src/tests/config/test_prompt_config.py 
b/hugegraph-llm/src/tests/config/test_prompt_config.py
index 0a46b4c6..c300a551 100644
--- a/hugegraph-llm/src/tests/config/test_prompt_config.py
+++ b/hugegraph-llm/src/tests/config/test_prompt_config.py
@@ -19,10 +19,14 @@ import json
 from pathlib import Path
 from unittest.mock import MagicMock
 
+import pytest
+
 from hugegraph_llm.config.prompt_config import PromptConfig
 from hugegraph_llm.models.llms.base import BaseLLM
 from hugegraph_llm.operators.llm_op.property_graph_extract import 
PropertyGraphExtract
 
+pytestmark = pytest.mark.contract
+
 
 def _json_objects_after_marker(prompt, marker):
     start = prompt.index(marker) + len(marker)
diff --git a/hugegraph-llm/src/tests/conftest.py 
b/hugegraph-llm/src/tests/conftest.py
index ff38f3d5..d2d1ed66 100644
--- a/hugegraph-llm/src/tests/conftest.py
+++ b/hugegraph-llm/src/tests/conftest.py
@@ -18,17 +18,72 @@
 import logging
 import os
 import sys
+from contextlib import suppress
 
 import nltk
+import pytest
 
 # Get project root directory
 project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), 
"../.."))
-# Add to Python path
 sys.path.insert(0, project_root)
 # Add src directory to Python path
 src_path = os.path.join(project_root, "src")
 sys.path.insert(0, src_path)
 
+from tests.fixtures.hugegraph_service import hugegraph_service  # noqa: E402
+
+__all__ = ["hugegraph_client", "hugegraph_service"]
+
+
+def _clear_quality_schema(client):
+    schema = client.schema()
+    for remove in (
+        lambda: schema.edgeLabel("quality_created").remove(),
+        lambda: schema.vertexLabel("quality_software").remove(),
+        lambda: schema.vertexLabel("quality_person").remove(),
+    ):
+        with suppress(Exception):
+            remove()
+
+
[email protected]()
+def hugegraph_client(hugegraph_service):
+    from pyhugegraph.client import PyHugeClient
+
+    from hugegraph_llm.config import huge_settings
+
+    original = {
+        "graph_url": huge_settings.graph_url,
+        "graph_name": huge_settings.graph_name,
+        "graph_user": huge_settings.graph_user,
+        "graph_pwd": huge_settings.graph_pwd,
+        "graph_space": huge_settings.graph_space,
+    }
+    huge_settings.graph_url = hugegraph_service.url
+    huge_settings.graph_name = hugegraph_service.graph
+    huge_settings.graph_user = hugegraph_service.user
+    huge_settings.graph_pwd = hugegraph_service.password
+    huge_settings.graph_space = hugegraph_service.graphspace
+
+    client = PyHugeClient(
+        url=hugegraph_service.url,
+        graph=hugegraph_service.graph,
+        user=hugegraph_service.user,
+        pwd=hugegraph_service.password,
+        graphspace=hugegraph_service.graphspace,
+    )
+    client.graphs().clear_graph_all_data()
+    _clear_quality_schema(client)
+    try:
+        yield client
+    finally:
+        try:
+            client.graphs().clear_graph_all_data()
+            _clear_quality_schema(client)
+        finally:
+            for key, value in original.items():
+                setattr(huge_settings, key, value)
+
 
 # Download NLTK resources
 def download_nltk_resources():
@@ -41,7 +96,8 @@ def download_nltk_resources():
 
 # Download NLTK resources before tests start
 download_nltk_resources()
-# Set environment variable to skip external service tests
-os.environ["SKIP_EXTERNAL_SERVICES"] = "true"
+# Default local tests away from external services while allowing selected
+# integration runs to opt in explicitly.
+os.environ.setdefault("SKIP_EXTERNAL_SERVICES", "true")
 # Log current Python path for debugging
 logging.debug("Python path: %s", sys.path)
diff --git 
a/hugegraph-llm/src/tests/data/quality_program/graphrag_documents.json 
b/hugegraph-llm/src/tests/data/quality_program/graphrag_documents.json
new file mode 100644
index 00000000..f66cb9cb
--- /dev/null
+++ b/hugegraph-llm/src/tests/data/quality_program/graphrag_documents.json
@@ -0,0 +1,4 @@
+[
+  {"id": "doc-1", "text": "marko is a person who created lop."},
+  {"id": "doc-2", "text": "lop is software written in java."}
+]
diff --git a/hugegraph-llm/src/tests/data/quality_program/kg_graph_output.json 
b/hugegraph-llm/src/tests/data/quality_program/kg_graph_output.json
new file mode 100644
index 00000000..d005c67b
--- /dev/null
+++ b/hugegraph-llm/src/tests/data/quality_program/kg_graph_output.json
@@ -0,0 +1,14 @@
+{
+  "vertices": [
+    {"label": "quality_person", "properties": {"name": "marko", "age": 29}},
+    {"label": "quality_software", "properties": {"name": "lop", "lang": 
"java"}}
+  ],
+  "edges": [
+    {
+      "label": "quality_created",
+      "outV": "quality_person:marko",
+      "inV": "quality_software:lop",
+      "properties": {"date": "2026-05-31"}
+    }
+  ]
+}
diff --git a/hugegraph-llm/src/tests/data/quality_program/kg_text.txt 
b/hugegraph-llm/src/tests/data/quality_program/kg_text.txt
new file mode 100644
index 00000000..be0663d5
--- /dev/null
+++ b/hugegraph-llm/src/tests/data/quality_program/kg_text.txt
@@ -0,0 +1 @@
+marko is a person aged 29. marko created lop. lop is software written in java.
diff --git 
a/hugegraph-llm/src/tests/data/quality_program/text2gremlin_schema.json 
b/hugegraph-llm/src/tests/data/quality_program/text2gremlin_schema.json
new file mode 100644
index 00000000..eba7dff8
--- /dev/null
+++ b/hugegraph-llm/src/tests/data/quality_program/text2gremlin_schema.json
@@ -0,0 +1,9 @@
+{
+  "vertexlabels": [
+    {"name": "quality_person", "properties": ["name", "age"]},
+    {"name": "quality_software", "properties": ["name", "lang"]}
+  ],
+  "edgelabels": [
+    {"name": "quality_created", "source_label": "quality_person", 
"target_label": "quality_software"}
+  ]
+}
diff --git a/hugegraph-llm/src/tests/document/test_document.py 
b/hugegraph-llm/src/tests/document/test_document.py
index cf106ead..cd5fe5da 100644
--- a/hugegraph-llm/src/tests/document/test_document.py
+++ b/hugegraph-llm/src/tests/document/test_document.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from hugegraph_llm.document import Document, Metadata
 
+pytestmark = pytest.mark.unit
+
 
 class TestDocument(unittest.TestCase):
     def test_document_initialization(self):
diff --git a/hugegraph-llm/src/tests/document/test_document_splitter.py 
b/hugegraph-llm/src/tests/document/test_document_splitter.py
index d1f67580..e919bddb 100644
--- a/hugegraph-llm/src/tests/document/test_document_splitter.py
+++ b/hugegraph-llm/src/tests/document/test_document_splitter.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from hugegraph_llm.document.chunk_split import ChunkSplitter
 
+pytestmark = pytest.mark.unit
+
 
 class TestChunkSplitter(unittest.TestCase):
     def test_paragraph_split_zh(self):
diff --git a/hugegraph-llm/src/tests/document/test_text_loader.py 
b/hugegraph-llm/src/tests/document/test_text_loader.py
index f9ac6730..529cfede 100644
--- a/hugegraph-llm/src/tests/document/test_text_loader.py
+++ b/hugegraph-llm/src/tests/document/test_text_loader.py
@@ -19,6 +19,10 @@ import os
 import tempfile
 import unittest
 
+import pytest
+
+pytestmark = pytest.mark.unit
+
 
 class TextLoader:
     """Simple text file loader for testing."""
diff --git a/hugegraph-llm/src/tests/config/test_config.py 
b/hugegraph-llm/src/tests/fixtures/__init__.py
similarity index 75%
copy from hugegraph-llm/src/tests/config/test_config.py
copy to hugegraph-llm/src/tests/fixtures/__init__.py
index 63dc6cb9..13a83393 100644
--- a/hugegraph-llm/src/tests/config/test_config.py
+++ b/hugegraph-llm/src/tests/fixtures/__init__.py
@@ -14,16 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-
-
-import unittest
-
-
-class TestConfig(unittest.TestCase):
-    def test_config(self):
-        import nltk
-
-        from hugegraph_llm.config import resource_path
-
-        nltk.data.path.append(resource_path)
-        nltk.data.find("corpora/stopwords")
diff --git a/hugegraph-llm/src/tests/config/test_config.py 
b/hugegraph-llm/src/tests/fixtures/fake_llm.py
similarity index 59%
copy from hugegraph-llm/src/tests/config/test_config.py
copy to hugegraph-llm/src/tests/fixtures/fake_llm.py
index 63dc6cb9..4eacd807 100644
--- a/hugegraph-llm/src/tests/config/test_config.py
+++ b/hugegraph-llm/src/tests/fixtures/fake_llm.py
@@ -16,14 +16,16 @@
 # under the License.
 
 
-import unittest
+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)
 
-class TestConfig(unittest.TestCase):
-    def test_config(self):
-        import nltk
-
-        from hugegraph_llm.config import resource_path
-
-        nltk.data.path.append(resource_path)
-        nltk.data.find("corpora/stopwords")
+    async def agenerate(self, prompt=None, messages=None, **kwargs):
+        return self.generate(prompt=prompt, messages=messages, **kwargs)
diff --git a/hugegraph-llm/src/tests/fixtures/hugegraph_service.py 
b/hugegraph-llm/src/tests/fixtures/hugegraph_service.py
new file mode 100644
index 00000000..a6318eb9
--- /dev/null
+++ b/hugegraph-llm/src/tests/fixtures/hugegraph_service.py
@@ -0,0 +1,74 @@
+# 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 os
+import time
+from dataclasses import dataclass
+
+import pytest
+import requests
+
+
+@dataclass(frozen=True)
+class HugeGraphService:
+    url: str
+    graph: str
+    user: str
+    password: str
+    graphspace: str | None
+
+
+def hugegraph_required() -> bool:
+    return os.getenv("HUGEGRAPH_REQUIRED", "false").lower() == "true"
+
+
+def hugegraph_service_from_env() -> HugeGraphService:
+    return HugeGraphService(
+        url=os.getenv("HUGEGRAPH_URL", "http://127.0.0.1:8080";),
+        graph=os.getenv("HUGEGRAPH_GRAPH", "hugegraph"),
+        user=os.getenv("HUGEGRAPH_USER", "admin"),
+        password=os.getenv("HUGEGRAPH_PASSWORD", "admin"),
+        graphspace=os.getenv("HUGEGRAPH_GRAPHSPACE") or None,
+    )
+
+
+def wait_for_hugegraph(service: HugeGraphService, timeout_seconds: int = 60) 
-> None:
+    deadline = time.monotonic() + timeout_seconds
+    last_error: Exception | None = None
+    while time.monotonic() < deadline:
+        try:
+            response = requests.get(f"{service.url}/versions", timeout=5)
+            response.raise_for_status()
+            return
+        except requests.RequestException as exc:
+            last_error = exc
+            time.sleep(2)
+    raise RuntimeError(f"HugeGraph is not ready at {service.url}/versions") 
from last_error
+
+
[email protected](scope="session")
+def hugegraph_service() -> HugeGraphService:
+    service = hugegraph_service_from_env()
+    if hugegraph_required():
+        wait_for_hugegraph(service)
+        return service
+
+    try:
+        wait_for_hugegraph(service, timeout_seconds=5)
+    except RuntimeError as exc:
+        pytest.skip(f"HugeGraph integration tests not selected with required 
service: {exc}")
+    return service
diff --git a/hugegraph-llm/src/tests/integration/test_core_graphrag_smoke.py 
b/hugegraph-llm/src/tests/integration/test_core_graphrag_smoke.py
new file mode 100644
index 00000000..c1fdac77
--- /dev/null
+++ b/hugegraph-llm/src/tests/integration/test_core_graphrag_smoke.py
@@ -0,0 +1,89 @@
+# 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 json
+from pathlib import Path
+
+import pytest
+
+pytestmark = [pytest.mark.smoke, pytest.mark.integration]
+
+
+class DeterministicEmbedding:
+    def get_embedding_dim(self):
+        return 2
+
+    def get_texts_embeddings(self, texts):
+        return [[float("lop" in text.lower()), float("marko" in text.lower())] 
for text in texts]
+
+    async def async_get_texts_embeddings(self, texts):
+        return self.get_texts_embeddings(texts)
+
+
+class InMemoryVectorIndex:
+    stores = {}
+
+    def __init__(self, name):
+        self.name = name
+        self.entries = []
+
+    @classmethod
+    def from_name(cls, embedding_dim, graph_name, index_name):
+        key = (embedding_dim, graph_name, index_name)
+        cls.stores.setdefault(key, cls(index_name))
+        return cls.stores[key]
+
+    def add(self, embeddings, chunks):
+        self.entries.extend(zip(embeddings, chunks))
+
+    def save_index_by_name(self, graph_name, index_name):
+        return None
+
+    def search(self, query_embedding, topk, dis_threshold=2):
+        scored = [(sum(a * b for a, b in zip(query_embedding, embedding)), 
chunk) for embedding, chunk in self.entries]
+        return [chunk for _, chunk in sorted(scored, reverse=True)[:topk]]
+
+
+def test_graphrag_smoke_uses_production_vector_and_rerank_operators():
+    from hugegraph_llm.operators.common_op.merge_dedup_rerank import 
MergeDedupRerank
+    from hugegraph_llm.operators.index_op.build_vector_index import 
BuildVectorIndex
+    from hugegraph_llm.operators.index_op.vector_index_query import 
VectorIndexQuery
+
+    InMemoryVectorIndex.stores.clear()
+    data_file = Path(__file__).resolve().parents[1] / "data" / 
"quality_program" / "graphrag_documents.json"
+    docs = json.loads(data_file.read_text(encoding="utf-8"))
+    chunks = [doc["text"] for doc in docs]
+    embedding = DeterministicEmbedding()
+
+    BuildVectorIndex(embedding=embedding, 
vector_index=InMemoryVectorIndex).run({"chunks": chunks})
+    vector_context = VectorIndexQuery(vector_index=InMemoryVectorIndex, 
embedding=embedding, topk=2).run(
+        {"query": "Who created lop?"}
+    )
+    merged_context = MergeDedupRerank(embedding=embedding, 
topk_return_results=2, method="bleu").run(
+        {
+            "query": "Who created lop?",
+            "vector_search": True,
+            "graph_search": True,
+            "vector_result": vector_context["vector_result"],
+            "graph_result": ["marko created lop"],
+        }
+    )
+
+    assert vector_context["vector_result"]
+    assert merged_context["graph_result"]
+    assert merged_context["vector_result"]
+    assert any("lop" in item for item in merged_context["vector_result"])
diff --git a/hugegraph-llm/src/tests/integration/test_core_kg_smoke.py 
b/hugegraph-llm/src/tests/integration/test_core_kg_smoke.py
new file mode 100644
index 00000000..2824a9f5
--- /dev/null
+++ b/hugegraph-llm/src/tests/integration/test_core_kg_smoke.py
@@ -0,0 +1,108 @@
+# 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 json
+from pathlib import Path
+
+import pytest
+
+pytestmark = [pytest.mark.smoke, pytest.mark.integration, 
pytest.mark.hugegraph]
+
+QUALITY_COMMIT_SCHEMA = {
+    "propertykeys": [
+        {"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"},
+        {"name": "age", "data_type": "INT", "cardinality": "SINGLE"},
+        {"name": "lang", "data_type": "TEXT", "cardinality": "SINGLE"},
+        {"name": "date", "data_type": "TEXT", "cardinality": "SINGLE"},
+    ],
+    "vertexlabels": [
+        {
+            "name": "quality_person",
+            "properties": ["name", "age"],
+            "primary_keys": ["name"],
+            "nullable_keys": [],
+        },
+        {
+            "name": "quality_software",
+            "properties": ["name", "lang"],
+            "primary_keys": ["name"],
+            "nullable_keys": [],
+        },
+    ],
+    "edgelabels": [
+        {
+            "name": "quality_created",
+            "source_label": "quality_person",
+            "target_label": "quality_software",
+            "properties": ["date"],
+        }
+    ],
+}
+
+
+def test_kg_construction_smoke_uses_production_code(hugegraph_client):
+    from hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph import 
Commit2Graph
+    from hugegraph_llm.operators.hugegraph_op.fetch_graph_data import 
FetchGraphData
+
+    fixture_file = Path(__file__).resolve().parents[1] / "data" / 
"quality_program" / "kg_graph_output.json"
+    fixture = json.loads(fixture_file.read_text(encoding="utf-8"))
+    assert fixture["vertices"]
+    assert fixture["edges"]
+
+    data = {
+        "schema": QUALITY_COMMIT_SCHEMA,
+        "vertices": fixture["vertices"],
+        "edges": fixture["edges"],
+    }
+    Commit2Graph().run(data)
+
+    summary = FetchGraphData(hugegraph_client).run({})
+    vertices = hugegraph_client.gremlin().exec(
+        """
+        g.V().hasLabel('quality_person', 'quality_software').
+          project('label', 'name', 'age', 'lang').
+          by(label()).
+          by(values('name')).
+          by(coalesce(values('age'), constant(null))).
+          by(coalesce(values('lang'), constant(null)))
+        """
+    )["data"]
+    edges = hugegraph_client.gremlin().exec(
+        """
+        g.E().hasLabel('quality_created').
+          project('label', 'out', 'in', 'date').
+          by(label()).
+          by(outV().values('name')).
+          by(inV().values('name')).
+          by(values('date'))
+        """
+    )["data"]
+
+    assert summary["vertex_num"] == len(fixture["vertices"])
+    assert summary["edge_num"] == len(fixture["edges"])
+    assert sorted(vertices, key=lambda item: item["label"]) == [
+        {"label": "quality_person", "name": "marko", "age": 29, "lang": None},
+        {"label": "quality_software", "name": "lop", "age": None, "lang": 
"java"},
+    ]
+    assert edges == [
+        {
+            "label": "quality_created",
+            "out": "marko",
+            "in": "lop",
+            "date": "2026-05-31",
+        }
+    ]
diff --git 
a/hugegraph-llm/src/tests/integration/test_core_text2gremlin_smoke.py 
b/hugegraph-llm/src/tests/integration/test_core_text2gremlin_smoke.py
new file mode 100644
index 00000000..3ebecd28
--- /dev/null
+++ b/hugegraph-llm/src/tests/integration/test_core_text2gremlin_smoke.py
@@ -0,0 +1,68 @@
+# 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 json
+from pathlib import Path
+
+import pytest
+
+from tests.fixtures.fake_llm import FakeLLM
+
+pytestmark = [pytest.mark.smoke, pytest.mark.integration]
+
+
+def test_text2gremlin_smoke_normalizes_fake_llm_output():
+    from hugegraph_llm.flows.text2gremlin import Text2GremlinFlow
+    from hugegraph_llm.operators.llm_op.gremlin_generate import 
GremlinGenerateSynthesize
+    from hugegraph_llm.state.ai_state import WkFlowInput
+
+    schema_file = Path(__file__).resolve().parents[1] / "data" / 
"quality_program" / "text2gremlin_schema.json"
+    schema = json.loads(schema_file.read_text(encoding="utf-8"))
+    generator = GremlinGenerateSynthesize(
+        llm=FakeLLM(
+            [
+                "```gremlin\ng.V().hasLabel('quality_person')\n```",
+                "Here is the query:\n```gremlin\ng.V().has('quality_person', 
'name', 'marko')\n```",
+            ]
+        ),
+        schema=schema,
+    )
+
+    result = generator.run({"query": "Find marko"})
+    prepared = WkFlowInput()
+    Text2GremlinFlow().prepare(
+        prepared,
+        query="Find marko",
+        example_num=99,
+        schema_input="hugegraph",
+        gremlin_prompt_input=None,
+        requested_outputs=["template_gremlin", "invalid_output"],
+    )
+
+    assert result["result"] == "g.V().has('quality_person', 'name', 'marko')"
+    assert result["raw_result"] == "g.V().hasLabel('quality_person')"
+    assert prepared.example_num == 10
+    assert prepared.requested_outputs == ["template_gremlin"]
+
+
+def test_text2gremlin_smoke_invalid_query_fails_explicitly():
+    from hugegraph_llm.operators.llm_op.gremlin_generate import 
GremlinGenerateSynthesize
+
+    generator = GremlinGenerateSynthesize(llm=FakeLLM(["g.V()", "g.V()"]))
+
+    with pytest.raises(ValueError, match="query is required"):
+        generator.run({"query": ""})
diff --git a/hugegraph-llm/src/tests/integration/test_graph_rag_pipeline.py 
b/hugegraph-llm/src/tests/integration/test_graph_rag_pipeline.py
index 243963c3..8a352e29 100644
--- a/hugegraph-llm/src/tests/integration/test_graph_rag_pipeline.py
+++ b/hugegraph-llm/src/tests/integration/test_graph_rag_pipeline.py
@@ -21,8 +21,12 @@ import tempfile
 import unittest
 from unittest.mock import MagicMock
 
+import pytest
+
 from tests.utils.mock import MockEmbedding
 
+pytestmark = [pytest.mark.external, pytest.mark.slow]
+
 
 class BaseLLM:
     def generate(self, prompt, **kwargs):
diff --git a/hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py 
b/hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py
new file mode 100644
index 00000000..581963a9
--- /dev/null
+++ b/hugegraph-llm/src/tests/integration/test_hugegraph_boundary.py
@@ -0,0 +1,187 @@
+# 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.
+
+from copy import deepcopy
+
+import pytest
+
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
+QUALITY_SCHEMA = {
+    "vertices": [
+        {"vertex_label": "quality_person", "properties": ["name", "age"], 
"primary_keys": ["name"]},
+        {"vertex_label": "quality_software", "properties": ["name", "lang"], 
"primary_keys": ["name"]},
+    ],
+    "edges": [
+        {
+            "edge_label": "quality_created",
+            "source_vertex_label": "quality_person",
+            "target_vertex_label": "quality_software",
+            "properties": ["date"],
+        }
+    ],
+}
+
+QUALITY_GRAPH = {
+    "vertices": [
+        {"label": "quality_person", "properties": {"name": "marko", "age": 
29}},
+        {"label": "quality_software", "properties": {"name": "lop", "lang": 
"java"}},
+    ],
+    "edges": [
+        {
+            "label": "quality_created",
+            "source": "marko",
+            "target": "lop",
+            "properties": {"date": "2026-05-31"},
+        }
+    ],
+}
+
+QUALITY_COMMIT_SCHEMA = {
+    "propertykeys": [
+        {"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"},
+        {"name": "age", "data_type": "INT", "cardinality": "SINGLE"},
+        {"name": "lang", "data_type": "TEXT", "cardinality": "SINGLE"},
+        {"name": "date", "data_type": "TEXT", "cardinality": "SINGLE"},
+    ],
+    "vertexlabels": [
+        {
+            "name": "quality_person",
+            "properties": ["name", "age"],
+            "primary_keys": ["name"],
+            "nullable_keys": [],
+        },
+        {
+            "name": "quality_software",
+            "properties": ["name", "lang"],
+            "primary_keys": ["name"],
+            "nullable_keys": [],
+        },
+    ],
+    "edgelabels": [
+        {
+            "name": "quality_created",
+            "source_label": "quality_person",
+            "target_label": "quality_software",
+            "properties": ["date"],
+        }
+    ],
+}
+
+QUALITY_COMMIT_DATA = {
+    "schema": QUALITY_COMMIT_SCHEMA,
+    "vertices": [
+        {"label": "quality_person", "properties": {"name": "marko", "age": 
29}},
+        {"label": "quality_software", "properties": {"name": "lop", "lang": 
"java"}},
+    ],
+    "edges": [
+        {
+            "label": "quality_created",
+            "outV": "quality_person:marko",
+            "inV": "quality_software:lop",
+            "properties": {"date": "2026-05-31"},
+        }
+    ],
+}
+
+
+def _create_quality_schema(client):
+    schema = client.schema()
+    schema.propertyKey("name").asText().ifNotExist().create()
+    schema.propertyKey("age").asInt().ifNotExist().create()
+    schema.propertyKey("lang").asText().ifNotExist().create()
+    schema.propertyKey("date").asText().ifNotExist().create()
+    schema.vertexLabel("quality_person").properties("name", 
"age").primaryKeys("name").ifNotExist().create()
+    schema.vertexLabel("quality_software").properties("name", 
"lang").primaryKeys("name").ifNotExist().create()
+    
schema.edgeLabel("quality_created").sourceLabel("quality_person").targetLabel("quality_software").properties(
+        "date"
+    ).ifNotExist().create()
+
+
+def _commit_quality_graph():
+    from hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph import 
Commit2Graph
+
+    commit = Commit2Graph()
+    data = deepcopy(QUALITY_COMMIT_DATA)
+    return commit.run(data)
+
+
+def test_schema_manager_reads_real_schema(hugegraph_client, hugegraph_service):
+    from hugegraph_llm.operators.hugegraph_op.schema_manager import 
SchemaManager
+
+    _create_quality_schema(hugegraph_client)
+
+    manager = SchemaManager(graph_name=hugegraph_service.graph)
+    context = manager.run({})
+
+    assert "schema" in context
+    assert "simple_schema" in context
+    assert isinstance(context["schema"]["vertexlabels"], list)
+    assert any(label["name"] == "quality_person" for label in 
context["schema"]["vertexlabels"])
+
+
+def test_commit_to_graph_writes_vertices_and_edges(hugegraph_client):
+    _commit_quality_graph()
+
+    counts = hugegraph_client.gremlin().exec(
+        """
+        g.V().hasLabel('quality_person', 'quality_software').count()
+        """
+    )["data"][0]
+    edges = hugegraph_client.gremlin().exec(
+        """
+        g.E().hasLabel('quality_created').
+          project('out', 'in', 'date').
+          by(outV().values('name')).
+          by(inV().values('name')).
+          by(values('date'))
+        """
+    )["data"]
+
+    assert counts == 2
+    assert len(edges) == 1
+    assert edges[0]["out"] == "marko"
+    assert edges[0]["in"] == "lop"
+    assert edges[0]["date"] == "2026-05-31"
+
+
+def test_fetch_graph_data_returns_counts_and_samples(hugegraph_client):
+    from hugegraph_llm.operators.hugegraph_op.fetch_graph_data import 
FetchGraphData
+
+    _commit_quality_graph()
+    result = FetchGraphData(hugegraph_client).run({})
+
+    assert {"vertex_num", "edge_num", "vertices", "edges", 
"note"}.issubset(result)
+    assert result["vertex_num"] == 2
+    assert result["edge_num"] == 1
+    assert isinstance(result["vertices"], list)
+    assert isinstance(result["edges"], list)
+
+
+def test_gremlin_execute_surfaces_invalid_query(hugegraph_client):
+    from hugegraph_llm.nodes.hugegraph_node.gremlin_execute import 
GremlinExecuteNode
+
+    node = GremlinExecuteNode()
+    node.wk_input = type("Input", (), {"requested_outputs": 
["raw_execution_result"]})()
+    result = node.operator_schedule({"raw_result": "g.V2()"})
+
+    assert result["template_exec_res"] == ""
+    assert (
+        "g.V2" in result["raw_exec_res"]
+        or "No signature" in result["raw_exec_res"]
+        or "NotFound" in result["raw_exec_res"]
+    )
diff --git a/hugegraph-llm/src/tests/integration/test_kg_construction.py 
b/hugegraph-llm/src/tests/integration/test_kg_construction.py
index daa7b191..379cb350 100644
--- a/hugegraph-llm/src/tests/integration/test_kg_construction.py
+++ b/hugegraph-llm/src/tests/integration/test_kg_construction.py
@@ -22,6 +22,8 @@ import os
 import unittest
 from unittest.mock import patch
 
+import pytest
+
 # 导入测试工具
 from src.tests.test_utils import (
     create_test_document,
@@ -29,6 +31,8 @@ from src.tests.test_utils import (
     with_mock_openai_client,
 )
 
+pytestmark = [pytest.mark.external, pytest.mark.slow]
+
 
 # Create mock classes to replace missing modules
 class OpenAILLM:
diff --git a/hugegraph-llm/src/tests/integration/test_rag_pipeline.py 
b/hugegraph-llm/src/tests/integration/test_rag_pipeline.py
index bb5e4364..3abf253f 100644
--- a/hugegraph-llm/src/tests/integration/test_rag_pipeline.py
+++ b/hugegraph-llm/src/tests/integration/test_rag_pipeline.py
@@ -19,6 +19,8 @@ import os
 import tempfile
 import unittest
 
+import pytest
+
 # 导入测试工具
 from src.tests.test_utils import (
     create_test_document,
@@ -28,6 +30,8 @@ from src.tests.test_utils import (
 )
 from tests.utils.mock import VectorIndex
 
+pytestmark = [pytest.mark.external, pytest.mark.slow]
+
 
 # 创建模拟类,替代缺失的模块
 class Document:
diff --git a/hugegraph-llm/src/tests/middleware/test_middleware.py 
b/hugegraph-llm/src/tests/middleware/test_middleware.py
index 90e71272..0a1cd36f 100644
--- a/hugegraph-llm/src/tests/middleware/test_middleware.py
+++ b/hugegraph-llm/src/tests/middleware/test_middleware.py
@@ -18,10 +18,13 @@
 import unittest
 from unittest.mock import AsyncMock, MagicMock, patch
 
+import pytest
 from fastapi import FastAPI
 
 from hugegraph_llm.middleware.middleware import UseTimeMiddleware
 
+pytestmark = pytest.mark.unit
+
 
 class TestUseTimeMiddlewareInit(unittest.TestCase):
     def setUp(self):
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..07b5df62 100644
--- a/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
+++ b/hugegraph-llm/src/tests/models/embeddings/test_ollama_embedding.py
@@ -20,6 +20,8 @@ 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
 
@@ -28,12 +30,14 @@ class TestOllamaEmbedding(unittest.TestCase):
     def setUp(self):
         self.skip_external = os.getenv("SKIP_EXTERNAL_SERVICES", 
"false").lower() == "true"
 
+    @pytest.mark.external
     @unittest.skipIf(os.getenv("SKIP_EXTERNAL_SERVICES", "false").lower() == 
"true", "Skipping external service tests")
     def test_get_text_embedding(self):
         ollama_embedding = 
OllamaEmbedding(model_name="quentinz/bge-large-zh-v1.5")
         embedding = ollama_embedding.get_text_embedding("hello world")
         print(embedding)
 
+    @pytest.mark.external
     @unittest.skipIf(os.getenv("SKIP_EXTERNAL_SERVICES", "false").lower() == 
"true", "Skipping external service tests")
     def test_get_cosine_similarity(self):
         ollama_embedding = 
OllamaEmbedding(model_name="quentinz/bge-large-zh-v1.5")
@@ -42,6 +46,7 @@ class TestOllamaEmbedding(unittest.TestCase):
         similarity = OllamaEmbedding.similarity(embedding1, embedding2, 
SimilarityMode.DEFAULT)
         print(similarity)
 
+    @pytest.mark.contract
     def test_async_get_texts_embeddings_preserves_batch_order(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.async_client = AsyncMock()
@@ -61,6 +66,7 @@ class TestOllamaEmbedding(unittest.TestCase):
 
         asyncio.run(run_async_test())
 
+    @pytest.mark.contract
     def test_async_get_text_embedding_requires_embeddings_key(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.async_client = AsyncMock()
@@ -74,6 +80,7 @@ class TestOllamaEmbedding(unittest.TestCase):
 
         asyncio.run(run_async_test())
 
+    @pytest.mark.contract
     def test_get_texts_embeddings_requires_embeddings_key(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.client = MagicMock()
@@ -82,6 +89,15 @@ class TestOllamaEmbedding(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "missing 'embeddings'"):
             ollama_embedding.get_texts_embeddings(["a"])
 
+    @pytest.mark.contract
+    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"])
+
+    @pytest.mark.contract
     def test_get_texts_embeddings_requires_non_empty_embeddings(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.client = MagicMock()
@@ -90,6 +106,7 @@ class TestOllamaEmbedding(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "returned no embeddings"):
             ollama_embedding.get_texts_embeddings(["a"])
 
+    @pytest.mark.contract
     def test_async_get_text_embedding_requires_non_empty_embeddings(self):
         ollama_embedding = OllamaEmbedding(model="test-model")
         ollama_embedding.async_client = AsyncMock()
@@ -102,3 +119,16 @@ class TestOllamaEmbedding(unittest.TestCase):
         import asyncio
 
         asyncio.run(run_async_test())
+
+    @pytest.mark.contract
+    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 3b27d780..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,14 +17,36 @@
 
 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
 
 from hugegraph_llm.models.llms.litellm import LiteLLMClient
 
+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 20e5aaac..cee6e9e0 100644
--- a/hugegraph-llm/src/tests/models/llms/test_openai_client.py
+++ b/hugegraph-llm/src/tests/models/llms/test_openai_client.py
@@ -19,8 +19,12 @@ import asyncio
 import unittest
 from unittest.mock import AsyncMock, MagicMock, patch
 
+import pytest
+
 from hugegraph_llm.models.llms.openai import OpenAIClient
 
+pytestmark = pytest.mark.contract
+
 
 class TestOpenAIClient(unittest.TestCase):
     def setUp(self):
@@ -109,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/hugegraph_op/test_commit_to_hugegraph.py 
b/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py
index b13f9042..fbd9e3a5 100644
--- a/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py
+++ b/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py
@@ -19,10 +19,12 @@
 import unittest
 from unittest.mock import MagicMock, patch
 
+import pytest
 from pyhugegraph.utils.exceptions import CreateError, NotFoundError
 
 from hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph import 
Commit2Graph
-from hugegraph_llm.operators.llm_op.property_graph_extract import 
PropertyGraphExtract
+
+pytestmark = [pytest.mark.unit]
 
 
 class TestCommit2Graph(unittest.TestCase):
@@ -298,256 +300,6 @@ class TestCommit2Graph(unittest.TestCase):
         # Verify that edgeLabel was called for each edge label
         self.assertEqual(schema_mocks["edge_label"].call_count, 1)  # 1 edge 
label
 
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._check_property_data_type")
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def test_load_into_graph(self, mock_handle_graph_creation, 
mock_check_property_data_type):
-        """Test load_into_graph method."""
-        # Setup mocks
-        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
-        mock_check_property_data_type.return_value = True
-
-        # Create vertices with proper data types according to schema
-        vertices = [
-            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
67}},
-            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
1994}},
-        ]
-
-        edges = [
-            {
-                "label": "acted_in",
-                "properties": {"role": "Forrest Gump"},
-                "outV": "person:Tom Hanks",  # Use the format expected by the 
implementation
-                "inV": "movie:Forrest Gump",  # Use the format expected by the 
implementation
-            }
-        ]
-
-        # Call the method
-        self.commit2graph.load_into_graph(vertices, edges, self.schema)
-
-        # Verify that _handle_graph_creation was called for each vertex and 
edge
-        self.assertEqual(mock_handle_graph_creation.call_count, 3)  # 2 
vertices + 1 edge
-
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def test_load_into_graph_with_data_type_validation_success(self, 
mock_handle_graph_creation):
-        """Test load_into_graph method with successful data type validation."""
-        # Setup mocks
-        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
-
-        # Create vertices with correct data types matching schema expectations
-        vertices = [
-            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
67}},  # age: INT -> int
-            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
1994}},  # year: INT -> int
-        ]
-
-        edges = [
-            {
-                "label": "acted_in",
-                "properties": {"role": "Forrest Gump"},  # role: TEXT -> str
-                "outV": "person:Tom Hanks",
-                "inV": "movie:Forrest Gump",
-            }
-        ]
-
-        # Call the method - should succeed with correct data types
-        self.commit2graph.load_into_graph(vertices, edges, self.schema)
-
-        # Verify that _handle_graph_creation was called for each vertex and 
edge
-        self.assertEqual(mock_handle_graph_creation.call_count, 3)  # 2 
vertices + 1 edge
-
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def test_load_into_graph_maps_llm_vertex_ids_to_created_vertex_ids(self, 
mock_handle_graph_creation):
-        """Test edges use server-created vertex ids when LLM ids differ."""
-        mock_handle_graph_creation.side_effect = [
-            MagicMock(id="1:Tom Hanks"),
-            MagicMock(id="2:Forrest Gump"),
-            MagicMock(id="edge_id"),
-        ]
-
-        vertices = [
-            {
-                "id": "person:Tom Hanks",
-                "label": "person",
-                "properties": {"name": "Tom Hanks", "age": 67},
-            },
-            {
-                "id": "movie:Forrest Gump",
-                "label": "movie",
-                "properties": {"title": "Forrest Gump", "year": 1994},
-            },
-        ]
-        edges = [
-            {
-                "label": "acted_in",
-                "properties": {"role": "Forrest Gump"},
-                "outV": "person:Tom Hanks",
-                "inV": "movie:Forrest Gump",
-            }
-        ]
-
-        self.commit2graph.load_into_graph(vertices, edges, self.schema)
-
-        self.assertEqual(vertices[0]["id"], "1:Tom Hanks")
-        self.assertEqual(vertices[1]["id"], "2:Forrest Gump")
-        mock_handle_graph_creation.assert_any_call(
-            self.commit2graph.client.graph().addEdge,
-            "acted_in",
-            "1:Tom Hanks",
-            "2:Forrest Gump",
-            {"role": "Forrest Gump"},
-        )
-
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def test_load_into_graph_uses_explicit_customize_string_ids(self, 
mock_handle_graph_creation):
-        """Test custom string ids are passed to HugeGraph when schema requires 
them."""
-        mock_handle_graph_creation.side_effect = [
-            MagicMock(id="Tom Hanks"),
-            MagicMock(id="Forrest Gump"),
-            MagicMock(id="edge_id"),
-        ]
-        schema = {
-            "propertykeys": [
-                {"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"},
-                {"name": "title", "data_type": "TEXT", "cardinality": 
"SINGLE"},
-            ],
-            "vertexlabels": [
-                {
-                    "id": 7,
-                    "name": "person",
-                    "id_strategy": "CUSTOMIZE_STRING",
-                    "primary_keys": ["name"],
-                    "properties": ["name"],
-                    "nullable_keys": [],
-                },
-                {
-                    "id": 8,
-                    "name": "movie",
-                    "id_strategy": "CUSTOMIZE_STRING",
-                    "primary_keys": ["title"],
-                    "properties": ["title"],
-                    "nullable_keys": [],
-                },
-            ],
-            "edgelabels": [{"name": "acted_in", "properties": [], 
"source_label": "person", "target_label": "movie"}],
-        }
-        vertices = [
-            {"id": "Tom Hanks", "label": "person", "properties": {"name": "Tom 
Hanks"}},
-            {"id": "Forrest Gump", "label": "movie", "properties": {"title": 
"Forrest Gump"}},
-        ]
-        edges = [
-            {
-                "label": "acted_in",
-                "properties": {},
-                "outV": "Tom Hanks",
-                "inV": "Forrest Gump",
-            }
-        ]
-
-        self.commit2graph.load_into_graph(vertices, edges, schema)
-
-        mock_handle_graph_creation.assert_any_call(
-            self.commit2graph.client.graph().addVertex,
-            "person",
-            {"name": "Tom Hanks"},
-            id="Tom Hanks",
-        )
-        mock_handle_graph_creation.assert_any_call(
-            self.commit2graph.client.graph().addVertex,
-            "movie",
-            {"title": "Forrest Gump"},
-            id="Forrest Gump",
-        )
-        mock_handle_graph_creation.assert_any_call(
-            self.commit2graph.client.graph().addEdge,
-            "acted_in",
-            "Tom Hanks",
-            "Forrest Gump",
-            {},
-        )
-
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def 
test_load_into_graph_accepts_normalized_extraction_without_item_type(self, 
mock_handle_graph_creation):
-        """Test normalized LLM output without type fields can be committed."""
-        mock_handle_graph_creation.side_effect = [
-            MagicMock(id="1:Tom Hanks"),
-            MagicMock(id="2:Forrest Gump"),
-            MagicMock(id="edge_id"),
-        ]
-        llm_output = """{
-            "vertices": [
-            {
-                "id": "person:Tom Hanks",
-                "label": "person",
-                "properties": {
-                    "name": "Tom Hanks",
-                    "age": 67
-                }
-            },
-            {
-                "id": "movie:Forrest Gump",
-                "label": "movie",
-                "properties": {
-                    "title": "Forrest Gump",
-                    "year": 1994
-                }
-            }
-            ],
-            "edges": [
-            {
-                "label": "acted_in",
-                "outV": "person:Tom Hanks",
-                "outVLabel": "person",
-                "inV": "movie:Forrest Gump",
-                "inVLabel": "movie",
-                "properties": {
-                    "role": "Forrest Gump"
-                }
-            }
-            ]
-        }"""
-
-        items = 
PropertyGraphExtract(llm=MagicMock())._extract_and_filter_label(self.schema, 
llm_output)
-        vertices = [item for item in items if item["type"] == "vertex"]
-        edges = [item for item in items if item["type"] == "edge"]
-        self.assertEqual(edges[0]["outV"], "1:Tom Hanks")
-        self.assertEqual(edges[0]["inV"], "2:Forrest Gump")
-
-        self.commit2graph.load_into_graph(vertices, edges, self.schema)
-
-        mock_handle_graph_creation.assert_any_call(
-            self.commit2graph.client.graph().addEdge,
-            "acted_in",
-            "1:Tom Hanks",
-            "2:Forrest Gump",
-            {"role": "Forrest Gump"},
-        )
-
-    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
-    def test_load_into_graph_with_data_type_validation_failure(self, 
mock_handle_graph_creation):
-        """Test load_into_graph method with data type validation failure."""
-        # Setup mocks
-        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
-
-        # Create vertices with incorrect data types (strings for INT fields)
-        vertices = [
-            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
"67"}},  # age should be int, not str
-            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
"1994"}},  # year should be int, not str
-        ]
-
-        edges = [
-            {
-                "label": "acted_in",
-                "properties": {"role": "Forrest Gump"},
-                "outV": "person:Tom Hanks",
-                "inV": "movie:Forrest Gump",
-            }
-        ]
-
-        # Call the method - should skip vertices due to data type validation 
failure
-        self.commit2graph.load_into_graph(vertices, edges, self.schema)
-
-        # Verify that _handle_graph_creation was called only for the edge 
(vertices were skipped)
-        self.assertEqual(mock_handle_graph_creation.call_count, 1)  # Only 1 
edge, vertices skipped
-
     def test_check_property_data_type_success(self):
         """Test _check_property_data_type method with valid data types."""
         # Test TEXT type
diff --git 
a/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph_load_into_graph.py
 
b/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph_load_into_graph.py
new file mode 100644
index 00000000..6605bec2
--- /dev/null
+++ 
b/hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph_load_into_graph.py
@@ -0,0 +1,366 @@
+# 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.
+
+# pylint: disable=protected-access,no-member
+import unittest
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph import 
Commit2Graph
+from hugegraph_llm.operators.llm_op.property_graph_extract import 
PropertyGraphExtract
+
+pytestmark = [pytest.mark.unit]
+
+
+class TestCommit2GraphLoadIntoGraph(unittest.TestCase):
+    def setUp(self):
+        self.mock_client = MagicMock()
+        self.mock_schema = MagicMock()
+        self.mock_client.schema.return_value = self.mock_schema
+
+        with patch(
+            
"hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.PyHugeClient", 
return_value=self.mock_client
+        ):
+            self.commit2graph = Commit2Graph()
+
+        self.schema = {
+            "propertykeys": [
+                {"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"},
+                {"name": "age", "data_type": "INT", "cardinality": "SINGLE"},
+                {"name": "title", "data_type": "TEXT", "cardinality": 
"SINGLE"},
+                {"name": "year", "data_type": "INT", "cardinality": "SINGLE"},
+                {"name": "role", "data_type": "TEXT", "cardinality": "SINGLE"},
+            ],
+            "vertexlabels": [
+                {
+                    "id": 1,
+                    "name": "person",
+                    "properties": ["name", "age"],
+                    "primary_keys": ["name"],
+                    "nullable_keys": ["age"],
+                    "id_strategy": "PRIMARY_KEY",
+                },
+                {
+                    "id": 2,
+                    "name": "movie",
+                    "properties": ["title", "year"],
+                    "primary_keys": ["title"],
+                    "nullable_keys": ["year"],
+                    "id_strategy": "PRIMARY_KEY",
+                },
+            ],
+            "edgelabels": [
+                {"name": "acted_in", "properties": ["role"], "source_label": 
"person", "target_label": "movie"}
+            ],
+        }
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._check_property_data_type")
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def test_load_into_graph(self, mock_handle_graph_creation, 
mock_check_property_data_type):
+        """Test load_into_graph method."""
+        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
+        mock_check_property_data_type.return_value = True
+
+        vertices = [
+            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
67}},
+            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
1994}},
+        ]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {"role": "Forrest Gump"},
+                "outV": "person:Tom Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        self.assertEqual(mock_handle_graph_creation.call_count, 3)
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def test_load_into_graph_with_data_type_validation_success(self, 
mock_handle_graph_creation):
+        """Test load_into_graph method with successful data type validation."""
+        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
+
+        vertices = [
+            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
67}},
+            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
1994}},
+        ]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {"role": "Forrest Gump"},
+                "outV": "person:Tom Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        self.assertEqual(mock_handle_graph_creation.call_count, 3)
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def test_load_into_graph_maps_llm_vertex_ids_to_created_vertex_ids(self, 
mock_handle_graph_creation):
+        """Test edges use server-created vertex ids when LLM ids differ."""
+        mock_handle_graph_creation.side_effect = [
+            MagicMock(id="1:Tom Hanks"),
+            MagicMock(id="2:Forrest Gump"),
+            MagicMock(id="edge_id"),
+        ]
+
+        vertices = [
+            {"id": "person:Tom Hanks", "label": "person", "properties": 
{"name": "Tom Hanks", "age": 67}},
+            {"id": "movie:Forrest Gump", "label": "movie", "properties": 
{"title": "Forrest Gump", "year": 1994}},
+        ]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {"role": "Forrest Gump"},
+                "outV": "person:Tom Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        self.assertEqual(vertices[0]["id"], "1:Tom Hanks")
+        self.assertEqual(vertices[1]["id"], "2:Forrest Gump")
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addEdge,
+            "acted_in",
+            "1:Tom Hanks",
+            "2:Forrest Gump",
+            {"role": "Forrest Gump"},
+        )
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def 
test_load_into_graph_maps_multiple_primary_keys_to_created_vertex_ids(self, 
mock_handle_graph_creation):
+        mock_handle_graph_creation.side_effect = [
+            MagicMock(id="1:Tom!Hanks"),
+            MagicMock(id="2:Forrest Gump"),
+            MagicMock(id="edge_id"),
+        ]
+        schema = {
+            "propertykeys": [
+                {"name": "first", "data_type": "TEXT", "cardinality": 
"SINGLE"},
+                {"name": "last", "data_type": "TEXT", "cardinality": "SINGLE"},
+                {"name": "title", "data_type": "TEXT", "cardinality": 
"SINGLE"},
+            ],
+            "vertexlabels": [
+                {
+                    "id": 1,
+                    "name": "person",
+                    "properties": ["first", "last"],
+                    "primary_keys": ["first", "last"],
+                    "nullable_keys": [],
+                    "id_strategy": "PRIMARY_KEY",
+                },
+                {
+                    "id": 2,
+                    "name": "movie",
+                    "properties": ["title"],
+                    "primary_keys": ["title"],
+                    "nullable_keys": [],
+                    "id_strategy": "PRIMARY_KEY",
+                },
+            ],
+            "edgelabels": [{"name": "acted_in", "properties": [], 
"source_label": "person", "target_label": "movie"}],
+        }
+        vertices = [
+            {"label": "person", "properties": {"first": "Tom", "last": 
"Hanks"}},
+            {"label": "movie", "properties": {"title": "Forrest Gump"}},
+        ]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {},
+                "outV": "person:Tom!Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        self.commit2graph.load_into_graph(vertices, edges, schema)
+
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addEdge,
+            "acted_in",
+            "1:Tom!Hanks",
+            "2:Forrest Gump",
+            {},
+        )
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def test_load_into_graph_uses_explicit_customize_string_ids(self, 
mock_handle_graph_creation):
+        """Test custom string ids are passed to HugeGraph when schema requires 
them."""
+        mock_handle_graph_creation.side_effect = [
+            MagicMock(id="Tom Hanks"),
+            MagicMock(id="Forrest Gump"),
+            MagicMock(id="edge_id"),
+        ]
+        schema = {
+            "propertykeys": [
+                {"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"},
+                {"name": "title", "data_type": "TEXT", "cardinality": 
"SINGLE"},
+            ],
+            "vertexlabels": [
+                {
+                    "id": 7,
+                    "name": "person",
+                    "id_strategy": "CUSTOMIZE_STRING",
+                    "primary_keys": ["name"],
+                    "properties": ["name"],
+                    "nullable_keys": [],
+                },
+                {
+                    "id": 8,
+                    "name": "movie",
+                    "id_strategy": "CUSTOMIZE_STRING",
+                    "primary_keys": ["title"],
+                    "properties": ["title"],
+                    "nullable_keys": [],
+                },
+            ],
+            "edgelabels": [{"name": "acted_in", "properties": [], 
"source_label": "person", "target_label": "movie"}],
+        }
+        vertices = [
+            {"id": "Tom Hanks", "label": "person", "properties": {"name": "Tom 
Hanks"}},
+            {"id": "Forrest Gump", "label": "movie", "properties": {"title": 
"Forrest Gump"}},
+        ]
+        edges = [{"label": "acted_in", "properties": {}, "outV": "Tom Hanks", 
"inV": "Forrest Gump"}]
+
+        self.commit2graph.load_into_graph(vertices, edges, schema)
+
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addVertex,
+            "person",
+            {"name": "Tom Hanks"},
+            id="Tom Hanks",
+        )
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addVertex,
+            "movie",
+            {"title": "Forrest Gump"},
+            id="Forrest Gump",
+        )
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addEdge,
+            "acted_in",
+            "Tom Hanks",
+            "Forrest Gump",
+            {},
+        )
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def 
test_load_into_graph_accepts_normalized_extraction_without_item_type(self, 
mock_handle_graph_creation):
+        """Test normalized LLM output without type fields can be committed."""
+        mock_handle_graph_creation.side_effect = [
+            MagicMock(id="1:Tom Hanks"),
+            MagicMock(id="2:Forrest Gump"),
+            MagicMock(id="edge_id"),
+        ]
+        llm_output = """{
+            "vertices": [
+            {
+                "id": "person:Tom Hanks",
+                "label": "person",
+                "properties": {
+                    "name": "Tom Hanks",
+                    "age": 67
+                }
+            },
+            {
+                "id": "movie:Forrest Gump",
+                "label": "movie",
+                "properties": {
+                    "title": "Forrest Gump",
+                    "year": 1994
+                }
+            }
+            ],
+            "edges": [
+            {
+                "label": "acted_in",
+                "outV": "person:Tom Hanks",
+                "outVLabel": "person",
+                "inV": "movie:Forrest Gump",
+                "inVLabel": "movie",
+                "properties": {
+                    "role": "Forrest Gump"
+                }
+            }
+            ]
+        }"""
+
+        items = 
PropertyGraphExtract(llm=MagicMock())._extract_and_filter_label(self.schema, 
llm_output)
+        vertices = [item for item in items if item["type"] == "vertex"]
+        edges = [item for item in items if item["type"] == "edge"]
+        self.assertEqual(edges[0]["outV"], "1:Tom Hanks")
+        self.assertEqual(edges[0]["inV"], "2:Forrest Gump")
+
+        self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        mock_handle_graph_creation.assert_any_call(
+            self.commit2graph.client.graph().addEdge,
+            "acted_in",
+            "1:Tom Hanks",
+            "2:Forrest Gump",
+            {"role": "Forrest Gump"},
+        )
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def 
test_load_into_graph_raises_explicit_error_when_vertex_creation_fails(self, 
mock_handle_graph_creation):
+        """Test failed vertex creation is reported before edge creation."""
+        mock_handle_graph_creation.return_value = None
+
+        vertices = [{"label": "person", "properties": {"name": "Tom Hanks", 
"age": 67}}]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {"role": "Forrest Gump"},
+                "outV": "person:Tom Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        with self.assertRaisesRegex(ValueError, "Failed to create vertex"):
+            self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        mock_handle_graph_creation.assert_called_once()
+
+    
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+    def test_load_into_graph_with_data_type_validation_failure(self, 
mock_handle_graph_creation):
+        """Test load_into_graph method with data type validation failure."""
+        mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
+
+        vertices = [
+            {"label": "person", "properties": {"name": "Tom Hanks", "age": 
"67"}},
+            {"label": "movie", "properties": {"title": "Forrest Gump", "year": 
"1994"}},
+        ]
+        edges = [
+            {
+                "label": "acted_in",
+                "properties": {"role": "Forrest Gump"},
+                "outV": "person:Tom Hanks",
+                "inV": "movie:Forrest Gump",
+            }
+        ]
+
+        self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+        self.assertEqual(mock_handle_graph_creation.call_count, 1)
diff --git 
a/hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py 
b/hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py
index 8527502d..44b51f44 100644
--- a/hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py
+++ b/hugegraph-llm/src/tests/operators/hugegraph_op/test_fetch_graph_data.py
@@ -18,8 +18,12 @@
 import unittest
 from unittest.mock import MagicMock
 
+import pytest
+
 from hugegraph_llm.operators.hugegraph_op.fetch_graph_data import 
FetchGraphData
 
+pytestmark = [pytest.mark.unit]
+
 
 class TestFetchGraphData(unittest.TestCase):
     def setUp(self):
diff --git 
a/hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py 
b/hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py
index b77ae01f..05d9659f 100644
--- a/hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py
+++ b/hugegraph-llm/src/tests/operators/hugegraph_op/test_schema_manager.py
@@ -18,8 +18,12 @@
 import unittest
 from unittest.mock import MagicMock, patch
 
+import pytest
+
 from hugegraph_llm.operators.hugegraph_op.schema_manager import SchemaManager
 
+pytestmark = [pytest.mark.unit]
+
 
 class TestSchemaManager(unittest.TestCase):
     def setUp(self):
diff --git a/hugegraph-llm/src/tests/operators/llm_op/test_disambiguate_data.py 
b/hugegraph-llm/src/tests/operators/llm_op/test_disambiguate_data.py
index 04ee4214..4bb3c321 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_disambiguate_data.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_disambiguate_data.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from hugegraph_llm.operators.llm_op.disambiguate_data import DisambiguateData
 
+pytestmark = pytest.mark.contract
+
 
 class TestDisambiguateData(unittest.TestCase):
     def setUp(self):
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 557eade8..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
@@ -21,8 +21,13 @@ import json
 import unittest
 from unittest.mock import AsyncMock, MagicMock, patch
 
+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
 
 
 class TestGremlinGenerateSynthesize(unittest.TestCase):
@@ -122,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_info_extract.py 
b/hugegraph-llm/src/tests/operators/llm_op/test_info_extract.py
index 80359bd5..e352a097 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_info_extract.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_info_extract.py
@@ -17,12 +17,16 @@
 
 import unittest
 
+import pytest
+
 from hugegraph_llm.operators.llm_op.info_extract import (
     InfoExtract,
     extract_triples_by_regex,
     extract_triples_by_regex_with_schema,
 )
 
+pytestmark = pytest.mark.contract
+
 
 class TestInfoExtract(unittest.TestCase):
     def setUp(self):
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 a2cc1bb5..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
@@ -20,8 +20,13 @@
 import unittest
 from unittest.mock import MagicMock, patch
 
+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
 
 
 class TestKeywordExtract(unittest.TestCase):
@@ -252,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 3eb49026..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
@@ -21,6 +21,8 @@ import json
 import unittest
 from unittest.mock import MagicMock, patch
 
+import pytest
+
 from hugegraph_llm.models.llms.base import BaseLLM
 from hugegraph_llm.operators.llm_op.property_graph_extract import (
     PropertyGraphExtract,
@@ -28,6 +30,9 @@ 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
 
 
 class TestPropertyGraphExtract(unittest.TestCase):
@@ -389,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)
diff --git a/hugegraph-python-client/src/pyhugegraph/utils/util.py 
b/hugegraph-python-client/src/pyhugegraph/utils/util.py
index b4a6f644..ffe0ff90 100644
--- a/hugegraph-python-client/src/pyhugegraph/utils/util.py
+++ b/hugegraph-python-client/src/pyhugegraph/utils/util.py
@@ -100,14 +100,17 @@ class ResponseValidation:
             if not self._strict and response.status_code == 404:
                 log.info("Resource %s not found (404)", path)
             else:
+                if response.status_code == 401:
+                    check_if_authorized(response)
+
                 try:
                     body = response.json()
                     if isinstance(body, dict):
                         status = body.get("status")
                         status_message = status.get("message") if 
isinstance(status, dict) else None
                         details = (
-                            body.get("exception")
-                            or body.get("message")
+                            body.get("message")
+                            or body.get("exception")
                             or status_message
                             or response.text
                             or "unknown error"
@@ -129,9 +132,7 @@ class ResponseValidation:
 
                 if response.status_code == 404:
                     raise NotFoundError(response.content) from e
-                if response.status_code == 400:
-                    raise Exception(f"Server Exception: {details}") from e
-                raise e
+                raise Exception(f"Server Exception: {details}") from e
 
         except Exception:  # pylint: disable=broad-exception-caught
             log.error("Unhandled exception occurred: %s", 
traceback.format_exc())
diff --git a/hugegraph-python-client/src/tests/api/test_auth.py 
b/hugegraph-python-client/src/tests/api/test_auth.py
index 1f6dec95..22218ded 100644
--- a/hugegraph-python-client/src/tests/api/test_auth.py
+++ b/hugegraph-python-client/src/tests/api/test_auth.py
@@ -18,10 +18,13 @@
 
 import unittest
 
+import pytest
 from pyhugegraph.utils.exceptions import NotFoundError
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestAuthManager(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_auth_routing.py 
b/hugegraph-python-client/src/tests/api/test_auth_routing.py
index e7a6e105..1fcd5a3d 100644
--- a/hugegraph-python-client/src/tests/api/test_auth_routing.py
+++ b/hugegraph-python-client/src/tests/api/test_auth_routing.py
@@ -20,6 +20,8 @@ from urllib.parse import urljoin
 import pytest
 from pyhugegraph.api.auth import AuthManager
 
+pytestmark = pytest.mark.contract
+
 
 class DummyCfg:
     def __init__(self, url, graphspace, gs_supported, graph_name):
diff --git a/hugegraph-python-client/src/tests/api/test_graph.py 
b/hugegraph-python-client/src/tests/api/test_graph.py
index a66c3fb9..4c93fe58 100644
--- a/hugegraph-python-client/src/tests/api/test_graph.py
+++ b/hugegraph-python-client/src/tests/api/test_graph.py
@@ -17,10 +17,13 @@
 
 import unittest
 
+import pytest
 from pyhugegraph.utils.exceptions import NotFoundError
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestGraphManager(unittest.TestCase):
     client = None
@@ -160,3 +163,15 @@ class TestGraphManager(unittest.TestCase):
         edge2 = self.graph.addEdge("knows", vertex2.id, vertex1.id, {"date": 
"2012-01-10"})
         edges = self.graph.getEdgesById([edge1.id, edge2.id])
         self.assertEqual(len(edges), 2)
+
+
+def test_graph_supports_primary_key_and_custom_string_id(client_utils):
+    graph = client_utils.graph
+    graph.addVertex("person", {"name": "quality_marko", "age": 29, "city": 
"Beijing"})
+    person = graph.getVertexByCondition(label="person", properties={"name": 
"quality_marko"}, limit=1)[0]
+    assert person.id is not None
+
+    graph.addVertex("book", {"name": "Quality Book", "price": 100}, 
id="quality-book-1")
+    book = graph.getVertexById("quality-book-1")
+    assert book.id == "quality-book-1"
+    assert book.properties["name"] == "Quality Book"
diff --git a/hugegraph-python-client/src/tests/api/test_graphs.py 
b/hugegraph-python-client/src/tests/api/test_graphs.py
index 13fe53b0..54bf10b6 100644
--- a/hugegraph-python-client/src/tests/api/test_graphs.py
+++ b/hugegraph-python-client/src/tests/api/test_graphs.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestGraphsManager(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_gremlin.py 
b/hugegraph-python-client/src/tests/api/test_gremlin.py
index 346812b5..1eb4a21d 100644
--- a/hugegraph-python-client/src/tests/api/test_gremlin.py
+++ b/hugegraph-python-client/src/tests/api/test_gremlin.py
@@ -24,6 +24,8 @@ from pyhugegraph.utils.exceptions import NotFoundError
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestGremlin(unittest.TestCase):
     client = None
@@ -134,3 +136,11 @@ class TestGremlinSetupBehavior(unittest.TestCase):
             client.gremlin = mock.Mock()
             with mock.patch.dict(os.environ, {"SKIP_GREMLIN_TESTS": "true"}), 
self.assertRaises(unittest.SkipTest):
                 TestGremlin.setUpClass()
+
+
+def test_gremlin_error_surface_is_explicit(client_utils):
+    with pytest.raises(NotFoundError) as exc_info:
+        client_utils.gremlin.exec("g.V2()")
+
+    message = str(exc_info.value)
+    assert "g.V2" in message or "No signature" in message or "NotFound" in 
message
diff --git a/hugegraph-python-client/src/tests/api/test_metric.py 
b/hugegraph-python-client/src/tests/api/test_metric.py
index 3e1a67ad..4970152d 100644
--- a/hugegraph-python-client/src/tests/api/test_metric.py
+++ b/hugegraph-python-client/src/tests/api/test_metric.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestMetricsManager(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_response_validation.py 
b/hugegraph-python-client/src/tests/api/test_response_validation.py
index 759d9718..77dc3c9a 100644
--- a/hugegraph-python-client/src/tests/api/test_response_validation.py
+++ b/hugegraph-python-client/src/tests/api/test_response_validation.py
@@ -18,9 +18,13 @@
 import unittest
 from unittest.mock import Mock
 
+import pytest
 import requests
+from pyhugegraph.utils.exceptions import NotAuthorizedError
 from pyhugegraph.utils.util import ResponseValidation
 
+pytestmark = pytest.mark.contract
+
 
 class TestResponseValidation(unittest.TestCase):
     def _mock_error_response(self, body, text):
@@ -51,6 +55,43 @@ class TestResponseValidation(unittest.TestCase):
         with self.assertRaisesRegex(Exception, "Server Exception: bad 
gremlin"):
             validator(response, "POST", "/gremlin")
 
+    def test_backend_error_envelope_preserves_message(self):
+        response = Mock(spec=requests.Response)
+        response.status_code = 500
+        response.text = '{"exception":"BackendException","message":"quality 
failure"}'
+        response.content = response.text.encode("utf-8")
+        response.json.return_value = {"exception": "BackendException", 
"message": "quality failure"}
+        response.request = Mock(body='{"gremlin":"g.V2()"}', 
url="http://127.0.0.1:8080/gremlin";)
+        response.raise_for_status.side_effect = 
requests.exceptions.HTTPError("500 Server Error")
+        validator = ResponseValidation()
+
+        with pytest.raises(Exception) as exc_info:
+            validator(response, method="POST", path="/gremlin")
+
+        assert "quality failure" in str(exc_info.value)
+
+    def test_malformed_error_body_uses_response_text(self):
+        response = self._mock_error_response(ValueError("not json"), "not 
json")
+        response.json.side_effect = ValueError("not json")
+        validator = ResponseValidation()
+
+        with self.assertRaisesRegex(Exception, "Server Exception: not json"):
+            validator(response, "POST", "/gremlin")
+
+    def test_unauthorized_error_preserves_not_authorized_type(self):
+        response = Mock(spec=requests.Response)
+        response.status_code = 401
+        response.text = 
'{"exception":"NotAuthorizedException","message":"Authentication failed"}'
+        response.content = response.text.encode("utf-8")
+        response.request = Mock(body="Empty body", 
url="http://127.0.0.1:8080/graphs";)
+        response.raise_for_status.side_effect = 
requests.exceptions.HTTPError("401 Client Error")
+        validator = ResponseValidation()
+
+        with pytest.raises(NotAuthorizedError) as exc_info:
+            validator(response, method="GET", path="/graphs")
+
+        assert "Please check your username and password" in str(exc_info.value)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/hugegraph-python-client/src/tests/api/test_schema.py 
b/hugegraph-python-client/src/tests/api/test_schema.py
index 4f91822c..fa1343bb 100644
--- a/hugegraph-python-client/src/tests/api/test_schema.py
+++ b/hugegraph-python-client/src/tests/api/test_schema.py
@@ -16,9 +16,14 @@
 # under the License.
 
 import unittest
+from contextlib import suppress
+
+import pytest
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestSchemaManager(unittest.TestCase):
     client = None
@@ -79,3 +84,40 @@ class TestSchemaManager(unittest.TestCase):
     def test_get_index_label(self):
         index_label = self.schema.getIndexLabel("personByCity")
         self.assertEqual(index_label.name, "personByCity")
+
+
+def test_schema_create_and_fetch_property_vertex_edge_index(client_utils):
+    schema = client_utils.schema
+    try:
+        schema.propertyKey("quality_name").asText().ifNotExist().create()
+        schema.propertyKey("quality_score").asInt().ifNotExist().create()
+        schema.vertexLabel("quality_person").properties("quality_name", 
"quality_score").primaryKeys(
+            "quality_name"
+        ).ifNotExist().create()
+        
schema.edgeLabel("quality_knows").sourceLabel("quality_person").targetLabel(
+            "quality_person"
+        ).ifNotExist().create()
+        schema.indexLabel("quality_person_by_score").onV("quality_person").by(
+            "quality_score"
+        ).range().ifNotExist().create()
+
+        full_schema = schema.getSchema()
+
+        assert "propertykeys" in full_schema
+        assert "vertexlabels" in full_schema
+        assert "edgelabels" in full_schema
+        assert "indexlabels" in full_schema
+        assert schema.getPropertyKey("quality_name").name == "quality_name"
+        assert schema.getVertexLabel("quality_person").name == "quality_person"
+        assert schema.getEdgeLabel("quality_knows").name == "quality_knows"
+        assert schema.getIndexLabel("quality_person_by_score").name == 
"quality_person_by_score"
+    finally:
+        for remove in (
+            lambda: schema.indexLabel("quality_person_by_score").remove(),
+            lambda: schema.edgeLabel("quality_knows").remove(),
+            lambda: schema.vertexLabel("quality_person").remove(),
+            lambda: schema.propertyKey("quality_score").remove(),
+            lambda: schema.propertyKey("quality_name").remove(),
+        ):
+            with suppress(Exception):
+                remove()
diff --git a/hugegraph-python-client/src/tests/api/test_task.py 
b/hugegraph-python-client/src/tests/api/test_task.py
index 599d8d1f..f5d3a2a4 100644
--- a/hugegraph-python-client/src/tests/api/test_task.py
+++ b/hugegraph-python-client/src/tests/api/test_task.py
@@ -17,10 +17,13 @@
 
 import unittest
 
+import pytest
 from pyhugegraph.utils.exceptions import NotFoundError
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestTaskManager(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_traverser.py 
b/hugegraph-python-client/src/tests/api/test_traverser.py
index bcd40acf..03106966 100644
--- a/hugegraph-python-client/src/tests/api/test_traverser.py
+++ b/hugegraph-python-client/src/tests/api/test_traverser.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestTraverserManager(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_variable.py 
b/hugegraph-python-client/src/tests/api/test_variable.py
index d75ac514..cdb86022 100644
--- a/hugegraph-python-client/src/tests/api/test_variable.py
+++ b/hugegraph-python-client/src/tests/api/test_variable.py
@@ -22,6 +22,8 @@ from pyhugegraph.utils.exceptions import NotFoundError
 
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestVariable(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/api/test_version.py 
b/hugegraph-python-client/src/tests/api/test_version.py
index 1ca4a1e2..71eb0562 100644
--- a/hugegraph-python-client/src/tests/api/test_version.py
+++ b/hugegraph-python-client/src/tests/api/test_version.py
@@ -17,8 +17,12 @@
 
 import unittest
 
+import pytest
+
 from ..client_utils import ClientUtils
 
+pytestmark = [pytest.mark.integration, pytest.mark.hugegraph]
+
 
 class TestVersion(unittest.TestCase):
     client = None
diff --git a/hugegraph-python-client/src/tests/client_utils.py 
b/hugegraph-python-client/src/tests/client_utils.py
index 1914cdb0..7d209cc6 100644
--- a/hugegraph-python-client/src/tests/client_utils.py
+++ b/hugegraph-python-client/src/tests/client_utils.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import os
+
 from pyhugegraph.client import PyHugeClient
 
 
@@ -26,7 +28,19 @@ class ClientUtils:
     GRAPHSPACE = None
     TIMEOUT = 10
 
-    def __init__(self):
+    def __init__(self, service=None):
+        if service is not None:
+            self.URL = service.url
+            self.GRAPH = service.graph
+            self.USERNAME = service.user
+            self.PASSWORD = service.password
+            self.GRAPHSPACE = service.graphspace
+        else:
+            self.URL = os.getenv("HUGEGRAPH_URL", self.URL)
+            self.GRAPH = os.getenv("HUGEGRAPH_GRAPH", self.GRAPH)
+            self.USERNAME = os.getenv("HUGEGRAPH_USER", self.USERNAME)
+            self.PASSWORD = os.getenv("HUGEGRAPH_PASSWORD", self.PASSWORD)
+            self.GRAPHSPACE = os.getenv("HUGEGRAPH_GRAPHSPACE") or 
self.GRAPHSPACE
         self.client = PyHugeClient(
             url=self.URL,
             user=self.USERNAME,
diff --git a/hugegraph-llm/src/tests/config/test_config.py 
b/hugegraph-python-client/src/tests/conftest.py
similarity index 55%
copy from hugegraph-llm/src/tests/config/test_config.py
copy to hugegraph-python-client/src/tests/conftest.py
index 63dc6cb9..c58cd6b8 100644
--- a/hugegraph-llm/src/tests/config/test_config.py
+++ b/hugegraph-python-client/src/tests/conftest.py
@@ -15,15 +15,26 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import pytest
 
-import unittest
+from .client_utils import ClientUtils
+from .fixtures.hugegraph_service import hugegraph_service
 
+__all__ = ["client_utils", "hugegraph_service"]
 
-class TestConfig(unittest.TestCase):
-    def test_config(self):
-        import nltk
 
-        from hugegraph_llm.config import resource_path
[email protected]()
+def client_utils(hugegraph_service):
+    utils = ClientUtils(service=hugegraph_service)
+    utils.clear_graph_all_data()
+    utils.init_property_key()
+    utils.init_vertex_label()
+    utils.init_edge_label()
+    yield utils
+    utils.clear_graph_all_data()
 
-        nltk.data.path.append(resource_path)
-        nltk.data.find("corpora/stopwords")
+
[email protected](scope="class", autouse=True)
+def require_hugegraph_for_marked_tests(request):
+    if request.node.get_closest_marker("hugegraph") is not None:
+        request.getfixturevalue("hugegraph_service")
diff --git a/hugegraph-llm/src/tests/config/test_config.py 
b/hugegraph-python-client/src/tests/fixtures/__init__.py
similarity index 75%
copy from hugegraph-llm/src/tests/config/test_config.py
copy to hugegraph-python-client/src/tests/fixtures/__init__.py
index 63dc6cb9..13a83393 100644
--- a/hugegraph-llm/src/tests/config/test_config.py
+++ b/hugegraph-python-client/src/tests/fixtures/__init__.py
@@ -14,16 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-
-
-import unittest
-
-
-class TestConfig(unittest.TestCase):
-    def test_config(self):
-        import nltk
-
-        from hugegraph_llm.config import resource_path
-
-        nltk.data.path.append(resource_path)
-        nltk.data.find("corpora/stopwords")
diff --git a/hugegraph-python-client/src/tests/fixtures/hugegraph_service.py 
b/hugegraph-python-client/src/tests/fixtures/hugegraph_service.py
new file mode 100644
index 00000000..324b17ea
--- /dev/null
+++ b/hugegraph-python-client/src/tests/fixtures/hugegraph_service.py
@@ -0,0 +1,75 @@
+# 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 os
+import time
+from dataclasses import dataclass
+
+import pytest
+import requests
+
+
+@dataclass(frozen=True)
+class HugeGraphService:
+    url: str
+    graph: str
+    user: str
+    password: str
+    graphspace: str | None
+
+
+def hugegraph_required() -> bool:
+    return os.getenv("HUGEGRAPH_REQUIRED", "false").lower() == "true"
+
+
+def hugegraph_service_from_env() -> HugeGraphService:
+    graphspace = os.getenv("HUGEGRAPH_GRAPHSPACE") or None
+    return HugeGraphService(
+        url=os.getenv("HUGEGRAPH_URL", "http://127.0.0.1:8080";),
+        graph=os.getenv("HUGEGRAPH_GRAPH", "hugegraph"),
+        user=os.getenv("HUGEGRAPH_USER", "admin"),
+        password=os.getenv("HUGEGRAPH_PASSWORD", "admin"),
+        graphspace=graphspace,
+    )
+
+
+def wait_for_hugegraph(service: HugeGraphService, timeout_seconds: int = 60) 
-> None:
+    deadline = time.monotonic() + timeout_seconds
+    last_error: Exception | None = None
+    while time.monotonic() < deadline:
+        try:
+            response = requests.get(f"{service.url}/versions", timeout=5)
+            response.raise_for_status()
+            return
+        except requests.RequestException as exc:
+            last_error = exc
+            time.sleep(2)
+    raise RuntimeError(f"HugeGraph is not ready at {service.url}/versions") 
from last_error
+
+
[email protected](scope="session")
+def hugegraph_service() -> HugeGraphService:
+    service = hugegraph_service_from_env()
+    if hugegraph_required():
+        wait_for_hugegraph(service)
+        return service
+
+    try:
+        wait_for_hugegraph(service, timeout_seconds=5)
+    except RuntimeError as exc:
+        pytest.skip(f"HugeGraph integration tests not selected with required 
service: {exc}")
+    return service
diff --git a/pyproject.toml b/pyproject.toml
index faa8c73c..2687ce93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -144,6 +144,18 @@ constraint-dependencies = [
     "python-dateutil~=2.9.0",
 ]
 
+[tool.pytest.ini_options]
+markers = [
+    "unit: fast deterministic tests without network or Docker",
+    "contract: public contract tests; may use mocks but verify stable 
behavior",
+    "integration: tests requiring a real local service such as HugeGraph",
+    "hugegraph: tests requiring HugeGraph Server",
+    "smoke: end-to-end-ish high-value smoke over production pipeline 
boundaries",
+    "external: tests requiring external provider credentials or non-HugeGraph 
services",
+    "slow: long-running tests excluded from default local loops",
+]
+addopts = "--strict-markers --strict-config"
+
 # for code format
 [tool.ruff]
 line-length = 120


Reply via email to