Files
onyx/backend/tests/integration
Evan LohnandClaude Opus 5.5 f6557e2021 feat(connectors): connectors without named checks get the design's generic row (#16206)
## Description

Backend PR for the connector checks redesign (plan item A7), stacked on
#16205. A source without named checks shows the design's generic row.

**The generic row**
- The fallback check is "Test connection". A pass gives "Connected".
- A failure of the legacy `validate_connector_settings` probe gets a
line and a description for its kind:

  | Kind | Line |
  |---|---|
  | Rejected sign-in (401) | Sign-in rejected |
  | Denied permission (403) | No permission for the connector |
  | Not found (404) | Not found at the source |
  | Other settings error | Settings not accepted |
  | Rate limit (429) | Rate limited by the source |
  | Connection error or timeout | Cannot reach the source |
  | Unknown error | Unexpected response from the source |

- A typed error keeps its class. An untyped error (for example a raw
`requests.HTTPError`) gets its class from its HTTP status, as the named
checks map them. The description names the status, e.g. "(HTTP 401)".
- The probe's own text stays the `message`, so Error Details show it.
- An error that gives its own row text keeps it.
- The check is like a sign-in: on a form run, an unreachable source
fails it (`fails_when_unreachable`).
- The blocking-validation recorder writes the same row, so stored
reports and draft runs agree. It keeps the original error class in
`error_type`, for support.
- The new module `capability_checks/generic_row.py` holds the row text.
The check and the recorder both use it.

**Connectors that run nothing**
- 23 connectors keep the no-op `validate_connector_settings` of
`BaseConnector` (for example GitLab, Gmail, Asana). The fallback check
passed for them without a request to the source.
- These sources now get no INDEXING check
(`factory.py:source_has_settings_probe`). The plan and the runs hold no
check, and the recorder writes no row for a pass. A failure (for example
at construction) is still recorded.
- The frontend already hides the card when no check applies. Phase C can
show "No checks for this connector yet" from the same empty list. No new
API field is necessary.

**Not in this PR**
- The EE perm-sync fallback ("Permission sync validation") keeps its
name and text. The design has no generic row for it.
- The generic row does not set `validates_binding`. It needs the full
config, so a create form could not unlock on it.

## How Has This Been Tested?

- New unit tests (`test_generic_check_row.py`): the pass line, each
failure kind (typed and from an HTTP status), the status in the
description and in Error Details, an error with its own row, the
form-run unreachable policy, and the recorder's rows (with no row for a
pass without a probe).
- Updated `test_registry_fallbacks.py` (name, pass line, no check
without a probe) and the ratchet test (an INDEXING check exists only
with named checks or a settings probe).
- Updated the integration test `test_capability_check_run.py`: a
mock-connector run now holds no check. I did not run integration tests.
- Connector, server and EE unit tests: 7878 passed, 1 failed. The
failure
(`test_browser_lifecycle.py::test_download_is_not_retried_and_does_not_relaunch_browser`)
also fails on the stack below. Pre-commit is clean.

## Additional Options

- [ ] [Optional] Please cherry-pick this PR to the latest release
version.
- [x] [Optional] Override Linear Check

---
_Generated by [Claude
Code](https://claude.ai/code/session_01KFMazQbuG8ui3jxBxm47sC)_

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Sources without named checks now show the design's generic "Test
connection" row instead of a pass-through fallback. Sources whose
connector keeps the no-op `validate_connector_settings` probe get no
INDEXING check.

- The fallback maps each failure kind of the legacy settings probe to a
line and description; a typed error keeps its class, an untyped one gets
its class from its HTTP status (401, 403, 404, 429), and the probe's own
text stays the message for Error Details.
- The blocking-validation recorder writes the same row, so draft runs
and stored reports agree, and a later pass on a source without a
settings probe replaces an earlier stale failure.
- Fallback checks keep the original stack trace in Error Details for
every error kind, and the EE permission-sync fallback keeps its existing
name and text, since the generic row does not apply to it.

<sup>Written for commit 3e05be6b05.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/onyx-dot-app/onyx/pull/16206?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/onyx-dot-app/onyx/16206?returnTo=https%3A%2F%2Fgithub.com%2Fonyx-dot-app%2Fonyx%2Fpull%2F16206&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a standardized **“Test connection”** capability check for
connectors with settings validation.
* Successful checks now display **“Connected”** with consistent results.
* Connection failures provide clearer, categorized error messages,
including relevant HTTP status details.

* **Bug Fixes**
* Capability-check results now correctly reflect successful validation
after an earlier connection failure.
* Sources without an applicable settings validation no longer display an
unnecessary indexing check.

* **Documentation**
* Clarified how connector validation checks appear in capability-check
reports.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:56:53 +00:00
..
…

Integration Tests

General Testing Overview

The integration tests are designed with a "manager" class and a "test" class for each type of object being manipulated (e.g., user, persona, credential):

  • Manager Class: Contains methods for each type of API call. Responsible for creating, deleting, and verifying the existence of an entity.
  • Test Class: Stores data for each entity being tested. This is our "expected state" of the object.

The idea is that each test can use the manager class to create (.create()) a "test*" object. It can then perform an operation on the object (e.g., send a request to the API) and then check if the "test*" object is in the expected state by using the manager class (.verify()) function.

Craft Kubernetes tests under tests/integration/tests/craft/k8s/ run in the dedicated pr-craft-k8s-tests.yml lane against a Helm-installed kind cluster with the real api_server, web_server, Celery workers, sandbox-proxy, and sandbox pods. Prefer the same API-manager shape there for API behavior. Use direct sandbox manager calls only for low-level Kubernetes contracts that are not exposed cleanly through public APIs; direct task/stub checks belong in tests/external_dependency_unit/craft/.

Instructions for Running Integration Tests Locally

The tests call the API through the manager classes in common_utils/managers, so the generated OpenAPI client is not needed. Use ods openapi all only if you want the schema or client for other work.

  1. Start the services Onyx depends on (for example with Docker). The test session runs the api_server in-process through TestClient and starts the Celery workers itself, so do not start those.
    • If you'd like to set environment variables, you can do so by creating a .env file in the onyx/backend/tests/integration/ directory.
    • Onyx MUST be configured with AUTH_TYPE=basic.
    • Enterprise tests run only with RUN_EE_TESTS=true. They also need a license: put it in ONYX_DEV_LICENSE and the session seeds it.
  2. Navigate to onyx/backend.
  3. Run the following command in the terminal:
    python -m dotenv -f .env run -- pytest -s tests/integration/tests/
    
    or to run all tests in a file:
    python -m dotenv -f .env run -- pytest -s tests/integration/tests/path_to/test_file.py
    
    or to run a single test:
    python -m dotenv -f .env run -- pytest -s tests/integration/tests/path_to/test_file.py::test_function_name
    

Running some single tests require the mock_connector_server container to be running. If the above doesn't work, navigate to backend/tests/integration/mock_services and run

docker compose -f docker-compose.mock-it-services.yml -p mock-it-services-stack up -d

You will have to modify the networks section of the docker-compose file to <your stack name>_default if you brought up the standard onyx services with a name different from the default onyx.

Guidelines for Writing Integration Tests

  • As authentication is currently required for all tests, each test should start by creating a user.
  • Each test should ideally focus on a single API flow.
  • The test writer should try to consider failure cases and edge cases for the flow and write the tests to check for these cases.
  • Every step of the test should be commented describing what is being done and what the expected behavior is.
  • A summary of the test should be given at the top of the test function as well!
  • When writing new tests, manager classes, manager functions, and test classes, try to copy the style of the other ones that have already been written.
  • Be careful for scope creep!
    • No need to overcomplicate every test by verifying after every single API call so long as the case you would be verifying is covered elsewhere (ideally in a test focused on covering that case).
    • An example of this is: Creating an admin user is done at the beginning of nearly every test, but we only need to verify that the user is actually an admin in the test focused on checking admin permissions. For every other test, we can just create the admin user and assume that the permissions are working as expected.

Scripting the LLM with mock_llm

The mock_llm fixture (from tests/integration/conftest.py) fakes the LLM provider. The session starts a scripted OpenAI-compatible server (tests/integration/mock_services/mock_llm_server/server.py) in a thread. For each test, the fixture makes a new script and an openai_compatible provider (model mock-model) that points at it, and sets that provider as the default. Requests go through the real provider layer (LiteLLM, streaming, retries).

The fixture yields a MockLLMScript. Tell the model what to say with conversations of replies:

from tests.integration.common_utils.managers.mock_llm import MockLLMScript
from tests.integration.mock_services.mock_llm_server.models import (
    Reply,
    RequestConditions,
    ToolCall,
)


def test_search_then_answer(admin_user: DATestUser, mock_llm: MockLLMScript) -> None:
    mock_llm.conversation(
        "chat",
        Reply(
            tool_calls=[
                ToolCall(
                    id="call_1", name="internal_search", arguments={"queries": ["pto"]}
                )
            ],
            conditions=RequestConditions(offers=["internal_search"]),
        ),
        Reply(
            text="Twenty days.",
            conditions=RequestConditions(has_results_for=["call_1"]),
        ),
    )
    response = ChatSessionManager.send_message(...)
    first, second = mock_llm.requests_in("chat")
    assert second.tool_result("call_1") is not None
  • Reply: one response with optional reasoning, text, and tool_calls. The finish reason is tool_calls when there are tool calls, else stop. required=False lets the reply stay unused.
  • Conversation: a name, conditions, and ordered replies. A request goes to the next unused reply of each conversation whose request conditions (and the reply's own) it meets. Use one conversation per independent flow, for example one per parallel Deep Research agent. Put an optional reply in its own conversation.
  • RequestConditions: every set field must hold: has_tools, offers, does_not_offer, has_results_for (tool call ids with a result in the history), tool_choice (auto, required, or none), and prompt_contains (system and user text only; use it as a last resort).
  • Tool-free requests: a request with no tools and no tool results (for example query rephrase, section selection, or session naming) goes only to a reply whose conversation or reply conditions set has_tools=False.
  • Default reply: a request that no reply accepts gets Script.default_reply (default text This is a mock LLM response.). The default reply is never used up. With Script(default_reply=None) such a request gets HTTP 400, and Onyx does not retry it.
  • When replies from two conversations accept a request with different responses, the server rejects it as ambiguous.
  • Recorded requests: mock_llm.requests lists every request (messages, tools, tool_choice, body) and what served it (conversation, reply_index, used_default_reply, error). Use requests_in(name) and tool_result(call_id).
  • At teardown the fixture restores the previous default provider, deletes the mock provider, and fails the test if a request got an error or a required reply was not used.
  • StreamedResponse.packets holds every placed packet as {"placement": {...}, "obj": {...}}.

See tests/llm_workflows/test_mock_llm_server.py. Tests that must patch Onyx or control the stream very finely belong in tests/external_dependency_unit/ with MockLLM.

The Playwright stack runs the same server as a compose service (deployment/docker_compose/docker-compose.mock-llm-test.yml). See web/tests/e2e/README.md.

Current Testing Limitations

Test coverage

  • All tests are probably not as high coverage as they could be.
  • The "connector" tests in particular are super bare bones because we will be reworking connector/cc_pair sometime soon.
  • Global Curator role is not thoroughly tested.
  • No auth is not tested at all.

Failure checking

  • While we test expected auth failures, we only check that it failed at all.
  • We dont check that the return codes are what we expect.
  • This means that a test could be failing for a different reason than expected.
  • We should ensure that the proper codes are being returned for each failure case.
  • We should also query the db after each failure to ensure that the db is in the expected state.

Scope/focus

  • The tests may be scoped sub-optimally.
  • The scoping of each test may be overlapping.

Current Testing Coverage

The current testing coverage should be checked by reading the comments at the top of each test file.

TODO: Testing Coverage

  • Persona permissions testing
  • Read only (and/or basic) user permissions
    • Ensuring proper permission enforcement using the chat/doc_search endpoints
  • No auth

Ideas for integration testing design

Combine the "test" and "manager" classes

This could make test writing a bit cleaner by preventing test writers from having to pass around objects into functions that the objects have a 1:1 relationship with.

Rework DocumentIndexClient

Right now, its used a fixture and has to be passed around between manager classes. Could just be built where its used