Skip to content

[feature][testing-harness] Add TestHarness Phase 3: end-to-end run against real Glean#63

Open
dishant-gupta-glean wants to merge 15 commits into
feature/v0-workstreamfrom
feature/testing-harness-phase3
Open

[feature][testing-harness] Add TestHarness Phase 3: end-to-end run against real Glean#63
dishant-gupta-glean wants to merge 15 commits into
feature/v0-workstreamfrom
feature/testing-harness-phase3

Conversation

@dishant-gupta-glean

Copy link
Copy Markdown

Problem

Phase 1 and Phase 2 validate connector logic without touching Glean's API. Before shipping a connector, teams need a way to do a bounded end-to-end smoke test — real source data, real Glean indexing — with a safety dial to limit how many items get crawled so they don't flood production.

Solution

Implement `run_end_to_end` and `run_end_to_end_async` as the final phase of the `TestHarness` testing model. Both methods:

  1. Apply per-client `max_items` via the same `_patched_clients` context manager used in Phase 2 (bounding the crawl without changing connector logic).
  2. Call the connector's real indexing entry-point against whatever `GLEAN_SERVER_URL` / `GLEAN_INDEXING_API_TOKEN` are configured in the environment.
  3. Return `None` — results are verified by inspecting Glean's search UI or API, not the mock client.

`run_end_to_end_async` dispatches to `connector.index_data_async()` for `BaseAsyncStreamingDatasourceConnector` subclasses and falls back to `connector.index_data()` for sync connectors.

TestHarness additions

def run_end_to_end(self, *, mode=IndexingMode.FULL, options=None) -> None: ...
async def run_end_to_end_async(self, *, mode=IndexingMode.FULL, options=None) -> None: ...

File layout

tests/unit_tests/testing/harness/test_harness_phase3.py  # 7 unit tests (no live Glean)

Unit tests use `patch.object` to mock `index_data` / `index_data_async` so they can run in CI without credentials. They verify:

  • Correct method is called with forwarded `mode` and `options`
  • Client attributes are restored after an exception
  • `max_items` wrapping is applied before `index_data` is called

Validation

uv run pytest tests/unit_tests/testing/harness/ -v   # 95 tests, all pass
uv run ruff check src/glean/indexing/testing/harness/ tests/unit_tests/testing/harness/

Notes

dishant-gupta-glean and others added 4 commits July 7, 2026 08:27
…esting

Introduces glean.indexing.testing.harness with TestConfig (YAML-driven
config) and TestHarness exposing run_full_mock / run_full_mock_async.
Phase 1 delegates to the existing run_connector / run_connector_async
helpers — no new mock infrastructure, full backward compatibility.

Phase 2 (integration) and Phase 3 (end-to-end) method stubs raise
NotImplementedError until subsequent PRs land.

TestHarness and TestConfig are also re-exported from
glean.indexing.testing so existing imports are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces recording/replay data-client wrappers (sync, streaming, async)
and CacheManifest (versioned fixture metadata as NDJSON + JSON manifest).

TestHarness.run_integration_test / run_integration_test_async now:
- Monkey-patches each named connector attribute with a recording wrapper
  on cache miss, or a replay wrapper on cache hit (matched by SDK version)
- Restores the original client after the run in all cases
- Respects max_items per-client config and refresh_cache flag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions

Adds permissions.py with extract_permission_refs (collects all user/group
identities from DocumentDefinition ACL fields) and assert_negative_identities_absent
(raises AssertionError if any listed identity appears in any document's
allowedUsers, allowedGroups, or allowedGroupIntersections).

run_integration_test and run_integration_test_async now run the assertion
automatically when config.negative_test_identities is non-empty, covering
email, datasource_user_id, and group intersection paths.

PermissionRefs, extract_permission_refs, and assert_negative_identities_absent
are exported from glean.indexing.testing.harness for advanced usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add run_end_to_end and run_end_to_end_async to TestHarness, completing
the three-phase testing model. Phase 3 calls connector.index_data() (or
index_data_async for async connectors) against the real Glean API with
per-client max_items applied via _patched_clients. Removes the now-stale
Phase 3 NotImplementedError stubs from test_harness_phase1.py and adds
test_harness_phase3.py with 7 unit tests that use patch.object to
validate wiring without requiring live Glean credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dishant-gupta-glean dishant-gupta-glean requested a review from a team as a code owner July 7, 2026 03:27
# Make it awaitable
import asyncio

mock_async.return_value = asyncio.coroutine(lambda: None)()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The async unit test test_async_connector_calls_index_data_async uses asyncio.coroutine(lambda: None)(), but asyncio.coroutine is deprecated and removed in Python 3.11+, which will cause an AttributeError and break tests on modern Python versions.

Suggested fix: Replace the use of asyncio.coroutine with a real async function or an already-resolved awaitable. For example:

async def _noop():
    return None

mock_async.return_value = _noop()

Alternatively, use unittest.mock.AsyncMock for index_data_async so that the mock is natively awaitable.

🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean

💬 Help us improve! Was this comment helpful? React with 👍 or 👎


Requires ``GLEAN_SERVER_URL`` (or ``GLEAN_INSTANCE``) and
``GLEAN_INDEXING_API_TOKEN`` environment variables to be set.
Calls ``connector.index_data()`` without any mock patching. Per-client

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The Phase 3 documentation for TestHarness.run_end_to_end states that it "Calls connector.index_data() without any mock patching", but the implementation wraps the call in _patched_clients(self._connector, self._clients, self._config), and tests assert that the connector's client is replaced during the run, so the docstring is misleading about patching behavior.

Suggested fix: Update the Phase 3 and run_end_to_end docstrings to clarify that while no network mocking of the Glean server is performed, connector clients are temporarily wrapped/patched (e.g., to enforce max_items). For example, rephrase to: "Calls connector.index_data() against a real Glean instance without mocking the Glean API; connector clients are temporarily wrapped to apply per-client max_items limits."

🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean

💬 Help us improve! Was this comment helpful? React with 👍 or 👎

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Phase 3 of the TestHarness to run a connector end-to-end against a real Glean instance (using environment-configured credentials), while still applying the existing max_items safety bound via the same client-patching mechanism used in Phase 2.

Changes:

  • Implement TestHarness.run_end_to_end() to patch configured clients (for max_items) and invoke connector.index_data() against real Glean.
  • Implement TestHarness.run_end_to_end_async() to call index_data_async() for async-streaming connectors and fall back to index_data() for sync connectors, with the same client patching.
  • Add new unit tests for Phase 3 behavior and remove Phase 3 “NotImplemented” stub assertions from Phase 1 tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/glean/indexing/testing/harness/harness.py Implements Phase 3 sync/async entrypoints and updates Phase 3 documentation.
tests/unit_tests/testing/harness/test_harness_phase3.py Adds unit tests validating Phase 3 method dispatch and client patch/restore behavior.
tests/unit_tests/testing/harness/test_harness_phase1.py Removes tests asserting Phase 3 methods raise NotImplementedError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +7
Unit tests here validate the method interface and max_items wiring without
making actual network calls. The end-to-end flow is covered by integration
tests (marked ``@pytest.mark.integration``) that require
``GLEAN_INDEXING_API_TOKEN`` in the environment and are not run in CI by
default.
Comment on lines +87 to +103
# Patch index_data so we don't need a real Glean server, but still
# let the client patching happen
original_index_data = connector.index_data

def _capture_and_call(mode, options):
# Verify the client is wrapped (not the original) during index_data
assert connector.data_client is not real_client
# Still call original so client gets exercised
original_index_data(mode=mode, options=options)

with patch.object(connector, "index_data", side_effect=_capture_and_call):
# This will fail at PushUploader (no creds), so just check AttributeError
# is not raised first — the wrapping is what we're testing
try:
harness.run_end_to_end()
except Exception:
pass # Expected — no Glean credentials in unit test env
Comment on lines +138 to +145
with patch.object(connector, "index_data_async") as mock_async:
mock_async.return_value = None
# Make it awaitable
import asyncio

mock_async.return_value = asyncio.coroutine(lambda: None)()
await harness.run_end_to_end_async()
mock_async.assert_called_once_with(mode=IndexingMode.FULL, options=None)
Comment on lines +364 to +365
Calls ``connector.index_data()`` without any mock patching. Per-client
``max_items`` from :attr:`config` are applied to bound the crawl.
Comment on lines +381 to +384
Raises:
NotImplementedError: Always, until PR 4 is merged.
RuntimeError: If ``GLEAN_INDEXING_API_TOKEN`` or the server URL is
missing from the environment (raised by the underlying
:mod:`~glean.indexing.common.glean_client` module).
Comment on lines +410 to +411
Raises:
NotImplementedError: Always, until PR 4 is merged.
RuntimeError: If environment variables for the Glean client are missing.
dishant-gupta-glean and others added 7 commits July 9, 2026 10:26
- config.py: treat `testing: null` in YAML as {} instead of crashing
- harness.py: clarify Phase 1 doc — only Glean side is mocked, not source
- harness.py: add public `clients` property (returns a copy of _clients)
- test_harness_phase1.py: assert on harness.clients instead of _clients

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- replay_client.py: add max_items param to all three replay wrappers so
  record and replay are capped consistently
- harness.py: pass max_items to replay wrappers in _wrap_client
- harness.py: check data.ndjson exists alongside manifest before cache hit
  to avoid hard failures on partial writes
- harness.py: move patching loop inside try/finally so partial patches are
  always reverted on _wrap_client exception
- recording_client.py: remove misleading "atomically" from module docstring
- test_harness_phase2.py: derive _SDK_VER dynamically via importlib.metadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- permissions.py: clarify docstring — email is preferred over
  datasource_user_id (not "whichever is set")
- permissions.py: document allowedGroupIntersections[*].requiredGroups
  coverage in assert_negative_identities_absent docstring
- testing/__init__.py: re-export PermissionRefs, extract_permission_refs,
  assert_negative_identities_absent from glean.indexing.testing top-level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- harness.py: clarify run_end_to_end docstring — Glean API is not mocked
  but clients registered via 'clients' arg are still wrapped for max_items;
  document MissingEnvironmentVariableError instead of RuntimeError for both
  sync and async variants
- test_harness_phase3.py: fix module docstring — no @pytest.mark.integration
  tests exist; live runs are manual
- test_harness_phase3.py: replace bare except-all in test_max_items_applied_to_client
  with a sentinel exception so assertion failures inside the side_effect are
  not accidentally swallowed
- test_harness_phase3.py: replace asyncio.coroutine (removed in Python 3.11)
  with AsyncMock; use assert_awaited_once_with

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Base automatically changed from feature/testing-harness-permissions to feature/testing-harness-phase2 July 10, 2026 13:32
Base automatically changed from feature/testing-harness-phase2 to feature/testing-harness-phase1 July 10, 2026 13:33
Base automatically changed from feature/testing-harness-phase1 to feature/v0-workstream July 10, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants