Skip to content

UN-3636 [DEV] Hermetic LLM mock hook for execute-path critical tests#2170

Open
chandrasekharan-zipstack wants to merge 17 commits into
mainfrom
feat/un-3636-execute-path-llm-mock
Open

UN-3636 [DEV] Hermetic LLM mock hook for execute-path critical tests#2170
chandrasekharan-zipstack wants to merge 17 commits into
mainfrom
feat/un-3636-execute-path-llm-mock

Conversation

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor

Stacked on #2164. Adds the deterministic-LLM capability the execute-path critical-path gaps need, without dragging real provider secrets into CI.

What

  • sdk1 hook (llm.py): one module-level _inject_mock_response + a one-line call at all four litellm.completion/acompletion sites. When UNSTRACT_LLM_MOCK_RESPONSE is set, litellm returns that string as the completion with fixed usage (10/20/30), tunable via DEFAULT_MOCK_RESPONSE_*; error sentinels like litellm.RateLimitError force error paths. Unset in production → no-op.
  • compose overlay: pass the env through worker-executor-v2 / worker-file-processing-v2 (the processes that run the LLM). Empty unless the rig/CI sets it.
  • unit tests: pin both our injector and the litellm mock contract (string + deterministic usage + error sentinel), so a litellm bump can't silently break hermetic execute-path coverage.

Why this shape

  • Single-place change, no cross-codebase adapter churn (unlike the retired NoOp adapter).
  • Hermetic + secret-free by construction — a cache-miss can't fall through to a real paid call (contrast: a record/replay proxy needs real keys to warm + as miss fallback).
  • Execute path tolerates a plain string (TEXT prompts stored verbatim; JSON prompts use tolerant repair) and never calls test_connection(), so a fixed mock runs every execute path to completion.

Not in this PR

The actual execute-path e2e tests (workflow-create-execute, usage-token-tracking, …) that consume this hook — those need the full compose stack booted to author the API flow correctly and will land next. Critical-path mappings are intentionally left unflipped until a real passing test exists (no false greens).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change adds deterministic LLM mocking, shared hermetic E2E provisioning, API deployment and ETL execution tests, expanded critical-path coverage, CI image caching, and dependency-aware test-group blocking and reporting.

Changes

Hermetic E2E coverage

Layer / File(s) Summary
Deterministic mock runtime
unstract/sdk1/src/unstract/sdk1/llm.py, unstract/sdk1/tests/test_mock_response.py, tests/rig/runtime.py, tests/compose/docker-compose.test.yaml
LLM completion paths inject UNSTRACT_LLM_MOCK_RESPONSE, with runtime defaults, Compose wiring, warning behavior, and unit coverage.
Shared workflow and storage provisioning
tests/e2e/conftest.py, tests/e2e/api_deployment/conftest.py, tests/e2e/etl/conftest.py
Fixtures provision mocked workflows, API deployments, and MinIO-backed ETL workflows with authenticated HTTP helpers and polling support.
E2E execution scenarios
tests/e2e/api_deployment/*, tests/e2e/workflows/*, tests/e2e/prompt_studio/*, tests/e2e/etl/*
Tests cover async delivery, fan-out rejoin behavior, workflow execution, token usage, Prompt Studio responses, and ETL output.
Coverage registration and CI execution
tests/critical_paths.yaml, tests/groups.yaml, tests/README.md, .github/workflows/ci-test.yaml
Critical paths and required E2E groups are updated, documentation describes the new setup, and CI uses Buildx-based image builds with caching.

Dependency-aware group execution

Layer / File(s) Summary
Dependency traversal and group blocking
tests/rig/groups.py, tests/rig/cli.py, tests/rig/tests/test_groups.py
The rig computes transitive dependencies, tracks failed groups, skips downstream groups with synthetic blocked results, and includes the MinIO pytest plugin.
Blocked result reporting
tests/rig/reporting.py, tests/rig/tests/test_reporting.py
Blocked status, icons, dependency notes, and non-green classification are implemented and tested.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant E2ETests
  participant ProvisionedWorkflow
  participant API
  participant Workers
  participant LLM
  E2ETests->>ProvisionedWorkflow: provision mocked workflow
  ProvisionedWorkflow->>API: create and configure workflow
  E2ETests->>API: dispatch execution
  API->>Workers: process execution
  Workers->>LLM: request completion with mock_response
  LLM-->>Workers: return canned answer
  Workers-->>API: update execution result
  API-->>E2ETests: deliver completed result
Loading

Suggested reviewers: jaseemjaskp, muhammad-ali-e, ritwik-g

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers What and Why, but most template sections are missing, including How, breakage impact, testing, and checklist. Fill in the missing template sections: How, breakage impact, migrations, env config, related issues, testing, screenshots, and checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 32.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes the main change: a hermetic LLM mock hook for execute-path tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/un-3636-execute-path-llm-mock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from feat/un-3636-critical-path-integration-tests to main July 14, 2026 10:31
Execute-path critical paths (workflow-create-execute, usage-token-tracking,
etc.) need a deterministic LLM without real provider secrets. Add a single
sdk1 escape hatch: when UNSTRACT_LLM_MOCK_RESPONSE is set, inject litellm's
mock_response so completions return that string with fixed usage (10/20/30),
tunable via DEFAULT_MOCK_RESPONSE_* and error sentinels like
"litellm.RateLimitError". No-op in production (env unset).

Wire the env through worker-executor-v2 / worker-file-processing-v2 in the e2e
compose overlay so the mock reaches the process that runs the LLM.

Covered by unit tests pinning both our injector and the litellm mock contract
(string + deterministic usage + error sentinel) so a litellm bump can't
silently break hermetic execute-path coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
@chandrasekharan-zipstack
chandrasekharan-zipstack force-pushed the feat/un-3636-execute-path-llm-mock branch from f26d44c to fc0d056 Compare July 14, 2026 12:06
chandrasekharan-zipstack and others added 6 commits July 14, 2026 18:39
Add e2e tests that exercise the full execute path hermetically using the
UNSTRACT_LLM_MOCK_RESPONSE hook (no real LLM/secret):

- api-deployment-run + usage-token-tracking: deploy a workflow as an API,
  POST a doc synchronously, assert the mocked answer and fixed 10/20/30
  usage come back as structured JSON.
- workflow-create-execute: two-step /workflow/execute/ then poll to
  COMPLETED; COMPLETED + a successful file is the hermetic proof (without
  the mock the completion would fail).

A shared provisioned_workflow fixture stands up a Prompt Studio-backed API
workflow (NoOp x2text/vectordb, chunk_size=0 so embedding/vectordb are
skipped). The rig seeds a canonical mock string in ComposeRuntime so both
the workers and pytest see the same value; tests skip if it is unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
- test_mock_response.py: split the module docstring summary line (D205 was
  failing pre-commit.ci, the only red hook on the PR).
- groups.yaml: drop `optional: true` from e2e-workflow / e2e-api-deployment.
  They now hold real execute-path tests rather than placeholders, so a red one
  should fail the build directly instead of only surfacing as a critical-path
  gap via --fail-on-critical-gap.
- README: the workflows/ and api_deployment/ dirs are no longer "(future)", and
  document UNSTRACT_LLM_MOCK_RESPONSE under E2E runtime — including that only
  completions are mocked (embedding.py is unhooked; chunk_size=0 is what keeps
  indexing off the real provider).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4tU5vyB4G6BgfAnGqjxLs
The comment block sat where #2176 reworded the e2e-login comment and inserted
a new group, creating a textual merge conflict against main. Move it inside
each group body; the optional: true removals merge cleanly on their own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4tU5vyB4G6BgfAnGqjxLs
Drop the restated-critical-path and mechanism-narrating comments across the
LLM mock and the execute-path e2e tests; keep only the constraints the code
can't show (litellm's fixed usage, the mandatory filter arg, the two-call
execute contract). Also drop the groups.yaml 'not optional' comment: the field
is self-evident and would go stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4tU5vyB4G6BgfAnGqjxLs
Three follow-ups from review:

- llm.py: warn once per process when UNSTRACT_LLM_MOCK_RESPONSE is active. The
  hatch was silent, so a stray env var in production would fake every
  completion and its billing with nothing in the logs.

- rig: implement the runtime-gate-skip TODO in cmd_run. Groups run in topo
  order, so a dep's result is known before its dependents start; a red dep
  means the precondition it gates does not hold and dependents would only
  produce noise against a half-up stack. Adds GroupManifest.transitive_deps and
  a 'blocked' GroupResult status which is never green, so a blocked group
  attests no coverage and --fail-on-critical-gap still catches a path that
  quietly stopped being covered. This mattered less while every platform
  dependent was optional; it does now that the execute-path groups gate.

- api-deployment e2e: the 10/20/30 usage assert now explains itself, since
  counts are summed per reason and a multiple means extra completions rather
  than broken tracking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4tU5vyB4G6BgfAnGqjxLs
Closes the remaining gaps in the registry; every path is now proof: marker.

- callback-result-delivery: async API execution (timeout=0). With a file to
  process, COMPLETED is written only by the chord callback, so a polled result
  proves the callback worker ran. Pins the one-shot read too — fetching a
  delivered result acknowledges it and the next read 406s, which a retry loop
  added later would otherwise swallow silently.

- workflow-execution-fan-out: multi-file execution, one batch per file. Needed
  MAX_PARALLEL_FILE_BATCHES on the *backend* (workers ask it for the value and
  only fall back to their own env); at the default of 1 the files share a batch
  and run serially, so the test would have passed proving nothing. File bodies
  differ because identical ones are deduplicated by hash on ingest.

- prompt-studio-fetch-response: the authoring surface. Async, so the answer is
  polled from prompt-output/ rather than the task status, which reports the
  executor finishing and not the callback having stored anything. Upload is
  content-sniffed and OSS takes PDF only, hence the real (if empty) PDF.

- pipeline-etl-execute: source connector to destination, over MinIO — the only
  storage connector the compose stack both boots and registers. Reuses the
  exported tool but needs its own workflow, since endpoints are per-workflow
  and the existing one is committed to the API deployment tests.

None of these have run: the e2e job is skipped while the PR is a draft. Locally
verified only as far as it goes here — rig validate, rig self-tests, ruff, and
pytest collection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4tU5vyB4G6BgfAnGqjxLs
@chandrasekharan-zipstack
chandrasekharan-zipstack marked this pull request as ready for review July 17, 2026 11:51
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds deterministic LLM-backed coverage for execute-path tests. The main changes are:

  • Adds an env-controlled LiteLLM mock response hook in the SDK.
  • Passes the mock setting into the worker services used by e2e runs.
  • Adds critical-path e2e tests for workflows, API deployment, fan-out, callbacks, Prompt Studio, and ETL.
  • Updates the rig and CI reporting around blocked groups, critical-path proofs, and e2e image builds.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
unstract/sdk1/src/unstract/sdk1/llm.py Adds the env-controlled LiteLLM mock hook at the completion call sites.
tests/rig/cli.py Adds dependency blocking, blocked-result persistence, and mock env setup for platform test runs.
tests/rig/reporting.py Adds blocked status handling in reports, summaries, and junit parsing.
tests/e2e/etl/conftest.py Adds MinIO-backed ETL workflow setup for source and destination endpoints.
tests/e2e/etl/test_pipeline_etl_execute.py Adds the ETL critical-path test that seeds MinIO, runs a pipeline, and checks destination output.

Reviews (9): Last reviewed commit: "UN-3636 [FIX] Keep frontend in e2e scope" | Re-trigger Greptile

Comment thread tests/e2e/etl/conftest.py
Comment thread tests/rig/cli.py
…ll minio

The e2e tier failed on two counts:

- api-deployment run/fan-out asserted COMPLETED on the synchronous POST, but a
  file-bearing execution only reaches COMPLETED once the chord callback rejoins
  — the sync path reports EXECUTING under CI load. All three api-deployment
  tests now dispatch async (timeout=0) and poll the status endpoint via shared
  dispatch_async/poll_delivered helpers, tolerating transient poll timeouts
  (the async test's earlier ReadTimeout). execution_id is parsed from status_api
  since the async PENDING body omits it as a top-level field.

- e2e-etl skipped ("minio client not installed"), leaving pipeline-etl-execute a
  gap. Add minio to the rig's injected pytest deps so the test runs.

Verified against a full local stack: api_deployment (3), workflow, etl,
prompt_studio all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
Comment thread tests/e2e/etl/conftest.py Fixed
Clears the SonarCloud S5332 clear-text-HTTP finding on the test fixture
(compose-internal MinIO has no TLS) by sourcing the scheme from env, and
trims the low-value minio dependency comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/e2e/api_deployment/conftest.py (1)

84-96: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No backoff on ConnectionError/Timeout retries.

except _TRANSIENT: continue (Lines 89-90) retries immediately with no sleep, unlike the other branches. A Timeout is naturally throttled by the 30s request timeout, but a fast-failing ConnectionError (e.g., connection reset) would busy-loop against the status endpoint for up to _POLL_TIMEOUT_SECONDS.

Proposed fix
         except _TRANSIENT:
+            time.sleep(2)
             continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/api_deployment/conftest.py` around lines 84 - 96, Update the
polling retry logic around the transient exception handler in the status-wait
function to sleep for the existing polling interval before continuing. Preserve
the current handling for successful responses and non-transient failures, while
ensuring fast-failing ConnectionError retries do not busy-loop.
tests/e2e/conftest.py (1)

116-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CSRF-wrapping _post/_patch helpers are reimplemented per file. Both sites attach X-CSRFToken from the session cookie and POST/PATCH with a fixed timeout — one root cause (no shared e2e HTTP helper), duplicated per file.

  • tests/e2e/conftest.py#L116-L131: keep as the canonical _post/_patch (accepting a requests.Session); consider exporting them for reuse instead of re-deriving per-module wrappers.
  • tests/e2e/prompt_studio/test_prompt_studio_fetch_response.py#L101-L103: replace the local _post with a thin call into tests/e2e/conftest.py's _post(pw.session, ...) instead of reimplementing the CSRF-attach logic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/conftest.py` around lines 116 - 131, The CSRF-aware HTTP helpers
are duplicated across e2e files. Keep tests/e2e/conftest.py lines 116-131 as the
canonical _post and _patch implementations, making them reusable if needed; in
tests/e2e/prompt_studio/test_prompt_studio_fetch_response.py lines 101-103,
remove the local _post wrapper and delegate to conftest.py’s _post using
pw.session, preserving the existing request arguments and timeout behavior.
tests/rig/cli.py (1)

449-457: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the failure predicate to avoid 3-way drift.

"Did this group fail" is now computed independently in three places with slightly different optional gating (cascade tracking at Line 478-481 ignores optional; the two overall_exit folds at Line 493-498 and 502-508 respect it). A future tweak to failure criteria (e.g. new non-failing exit code) risks updating only some sites.

♻️ Suggested consolidation
+def _group_failed(exit_code: int, result: GroupResult | None) -> bool:
+    return exit_code not in _NON_FAILING_PYTEST_EXIT_CODES or (
+        result is not None and (result.errors or result.failed)
+    )
+
             if result is not None:
                 group_results.append(result)
             # Tracked regardless of `optional` — see the cascade note above.
-            if exit_code not in _NON_FAILING_PYTEST_EXIT_CODES or (
-                result is not None and (result.errors or result.failed)
-            ):
+            if _group_failed(exit_code, result):
                 failed_groups.add(name)
             ...
-            if (
-                exit_code not in _NON_FAILING_PYTEST_EXIT_CODES
-                and not group.optional
-                and overall_exit == 0
-            ):
+            if _group_failed(exit_code, None) and not group.optional and overall_exit == 0:
                 overall_exit = exit_code
-            if (
-                result is not None
-                and (result.errors or result.failed)
-                and not group.optional
-                and overall_exit == 0
-            ):
+            if _group_failed(0, result) and not group.optional and overall_exit == 0:
                 overall_exit = 1

Also applies to: 477-481, 493-508

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/rig/cli.py` around lines 449 - 457, Extract a shared failure predicate
for group results and use it consistently in the runnable-group cascade tracking
and both overall_exit folds. Ensure the predicate applies the same
optional-group gating everywhere, replacing the duplicated exit-code checks
while preserving blocked-result handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/conftest.py`:
- Around line 250-263: Track whether the workflow endpoint setup loop in the
fixture successfully patches at least one matching endpoint to API, and assert
that this occurred after iterating through eps. Keep the existing matching and
patch status assertion, but fail immediately with a clear fixture-level message
when eps is empty or no endpoint matches workflow_id.

---

Nitpick comments:
In `@tests/e2e/api_deployment/conftest.py`:
- Around line 84-96: Update the polling retry logic around the transient
exception handler in the status-wait function to sleep for the existing polling
interval before continuing. Preserve the current handling for successful
responses and non-transient failures, while ensuring fast-failing
ConnectionError retries do not busy-loop.

In `@tests/e2e/conftest.py`:
- Around line 116-131: The CSRF-aware HTTP helpers are duplicated across e2e
files. Keep tests/e2e/conftest.py lines 116-131 as the canonical _post and
_patch implementations, making them reusable if needed; in
tests/e2e/prompt_studio/test_prompt_studio_fetch_response.py lines 101-103,
remove the local _post wrapper and delegate to conftest.py’s _post using
pw.session, preserving the existing request arguments and timeout behavior.

In `@tests/rig/cli.py`:
- Around line 449-457: Extract a shared failure predicate for group results and
use it consistently in the runnable-group cascade tracking and both overall_exit
folds. Ensure the predicate applies the same optional-group gating everywhere,
replacing the duplicated exit-code checks while preserving blocked-result
handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13ad4ba2-a18b-4cb3-b034-91f3430b5f52

📥 Commits

Reviewing files that changed from the base of the PR and between 0786a0a and faa0d35.

📒 Files selected for processing (25)
  • tests/README.md
  • tests/compose/docker-compose.test.yaml
  • tests/critical_paths.yaml
  • tests/e2e/api_deployment/__init__.py
  • tests/e2e/api_deployment/conftest.py
  • tests/e2e/api_deployment/test_api_deployment_async.py
  • tests/e2e/api_deployment/test_api_deployment_fan_out.py
  • tests/e2e/api_deployment/test_api_deployment_run.py
  • tests/e2e/conftest.py
  • tests/e2e/etl/__init__.py
  • tests/e2e/etl/conftest.py
  • tests/e2e/etl/test_pipeline_etl_execute.py
  • tests/e2e/prompt_studio/__init__.py
  • tests/e2e/prompt_studio/test_prompt_studio_fetch_response.py
  • tests/e2e/workflows/__init__.py
  • tests/e2e/workflows/test_workflow_execute.py
  • tests/groups.yaml
  • tests/rig/cli.py
  • tests/rig/groups.py
  • tests/rig/reporting.py
  • tests/rig/runtime.py
  • tests/rig/tests/test_groups.py
  • tests/rig/tests/test_reporting.py
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/tests/test_mock_response.py

Comment thread tests/e2e/conftest.py
chandrasekharan-zipstack and others added 2 commits July 20, 2026 12:53
Assert at least one endpoint was patched to API in the provisioning
fixture, so a field-name mismatch surfaces here instead of as a confusing
downstream execute failure (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
The e2e job runs on ephemeral GitHub-hosted runners, so a plain
`docker compose build` rebuilt every image cold each run (~3min). Switch
to docker/bake-action with per-service type=gha cache scopes matching the
release workflow, so each runner restores layers from the shared GHA cache
(including entries the release build already wrote). load=true exports the
images into the daemon for the compose stack to boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/workflows/ci-test.yaml (1)

171-178: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Pin GitHub Actions to specific commit SHAs.

The newly added setup-buildx-action and bake-action use mutable version tags (@v4 and @v7). Consider pinning them to specific commit SHAs to maintain consistency with the checkout, login-action, and setup-uv steps earlier in this workflow and to strengthen supply chain security against upstream tag hijacking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-test.yaml around lines 171 - 178, Pin the newly added
docker/setup-buildx-action and docker/bake-action steps to immutable commit SHAs
instead of the mutable v4 and v7 tags, preserving their current action versions
and configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci-test.yaml:
- Around line 171-178: Pin the newly added docker/setup-buildx-action and
docker/bake-action steps to immutable commit SHAs instead of the mutable v4 and
v7 tags, preserving their current action versions and configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4e93703-fb7d-4774-aa7c-c19dd594ce6a

📥 Commits

Reviewing files that changed from the base of the PR and between 540a035 and d409dac.

📒 Files selected for processing (1)
  • .github/workflows/ci-test.yaml

SonarCloud flagged the two floating action tags added for the e2e gha
cache as new MAJOR vulnerabilities (security rating C). Pin to the commit
SHA the tag points to so a moved tag can't inject code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
Comment thread .github/workflows/ci-test.yaml Fixed
Comment thread .github/workflows/ci-test.yaml Fixed
e2e ran on every non-draft PR regardless of paths, so image-only or
docs-only changes triggered a full ~7min platform boot for nothing.

Add an `e2e_relevant` path scope that skips frontend/** and docs/**
while keeping docker/** in scope (image changes must still be
e2e-tested), gate the e2e job on it, and accept a skipped e2e as a
pass in the report gate (matching how a skipped test tier is handled).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L549habQ6K2m1ACJ7DwtsE

@ritwik-g ritwik-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review — UN-3636 execute-path e2e

Verified against the actual head commit, the live backend source, and the PR's own green CI run (29740023152): all six new e2e tests genuinely executed and passed there (e2e-workflow 1, e2e-prompt-studio 1, e2e-etl 1, e2e-api-deployment 3), and all seven test_mock_response.py tests pass with no provider key. That evidence retires a lot of speculation — bake builds and loads correctly, e2e_relevant does evaluate true, the mock reaches the workers, the adapter/connector UUIDs resolve, and the critical-path markers all map to real tests.

Findings below are what survived that verification. Nothing here disputes the approach — the injection-site hook is the right shape, and removing optional: true plus wiring dep-failure cascade genuinely strengthens the gate.

One P1, in the CI gate rather than the test code.

Note on the PR description (not a code finding)

The description is stale and now contradicts the diff. It still says under "Not in this PR" that the execute-path e2e tests "will land next" and that "critical-path mappings are intentionally left unflipped until a real passing test exists" — but this PR contains exactly those tests and flips six mappings in critical_paths.yaml. Worth rewriting before merge so the merge commit describes what actually shipped.

PR title conforms to TICKET [TYPE] TITLE.

Posted as COMMENT, not an approval.

e2e_result="${{ needs.e2e.result }}"
[ "$test_result" = "success" ] || [ "$test_result" = "skipped" ] || exit 1
[ "$e2e_result" = "success" ] || exit 1
[ "$e2e_result" = "success" ] || [ "$e2e_result" = "skipped" ] || exit 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 — a failed changes job now turns the required check green with zero tests run. [verified: read the full file; needs.changes appears only at :60 and :128, needs.changes.result is never checked anywhere]

This PR adds needs: changes to e2e (:128). GitHub sets a job's result to skipped when an upstream needs job fails or is cancelled. So if changes fails for any reason — paths-filter error, checkout failure, runner death — then:

  • needs.test.result == skipped → passes line 343
  • needs.e2e.result == skipped → passes line 344
  • report still runs (if: always(), :129) and the single branch-protection check reports success

with not one test having executed.

This is a regression introduced here. Before this PR e2e had no needs: at all, so a broken changes job still left e2e running and [ "$e2e_result" = "success" ] || exit 1 caught it. The comment that guarded exactly this ("e2e always runs, so it must be green") was removed in the same hunk, and changes is in report's needs list (:125) but its result is never consulted.

One line fixes it:

[ "${{ needs.changes.result }}" = "success" ] || exit 1

Related, unverified: workflow_dispatch is a declared trigger (:20). If paths-filter resolves its base to the default branch, a manual run on main diffs main-against-main → e2e_relevant=false → e2e skipped → green via the same path. Pre-existing for test, but this PR newly extends it to e2e. Worth confirming.

Comment thread tests/e2e/api_deployment/conftest.py Outdated
resp = deployment.session.get(
status_url, headers=deployment.auth, params=params, timeout=30
)
except _TRANSIENT:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — the transient-retry path is unsound against this specific endpoint, and can convert a network blip into a hard test failure. [verified: read backend/api_v2/api_deployment_views.py get() :165-237 and backend/workflow_manager/workflow_v2/workflow_helper.py :401-431]

Three problems, one root cause — the retry was written without the endpoint's real status-code surface or its acknowledge-on-read semantics.

1. A lost 200 makes the retry fail the test. In workflow_helper.py, result_acknowledged is snapshotted at :403 before _set_result_acknowledge(execution) runs at :409, and that call also does ResultCacheUtils.delete_api_results(...). The server therefore acknowledges and deletes the result before the response leaves. If that response is lost in transit — precisely the Timeout/ConnectionError this handler exists to tolerate — the retry gets 406, and line 93's assert resp.status_code == 422 fails with poll: HTTP 406. The retry is unsafe against the one endpoint it retries. The async test's own comment (test_api_deployment_async.py:41-43) documents this destructive read, so the hazard is already known — it just isn't handled here.

2. assert resp.status_code == 422 doesn't match the contract. The view can return 400 (:174), 500 (:208, contains_tool_not_found_error), 406 (:193), 422 (:218/:221) and 200 (:223). A tool-not-found deployment state returns 500, and the loop dies on poll: HTTP 500 — presented as a poll-protocol violation rather than the real diagnosis, which the 500 body actually contains.

3. continue at :90 has no sleep. The time.sleep(2) at :96 is inside the 422 branch only. A ConnectionError from a refused connection returns in microseconds (timeout=30 is a connect/read timeout, not a backoff), so a down backend produces an unthrottled request loop for the full 300s. And because last is only assigned at :94, the terminal message becomes result never delivered within 300s; last: — five minutes of spinning reported with zero evidence, since the exception is never captured.

Suggested shape: sleep on every iteration, capture the exception into last, treat 406 as "already delivered" rather than an assert, and let non-{200,406,422} surface as a real error rather than a poll assert.

- "!docs/**"
e2e_relevant:
- "!frontend/**"
- "!docs/**"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — frontend-only PRs now lose all image-build coverage. [verified: read ci-frontend-lint.yaml and docker/dockerfiles/frontend.Dockerfile]

e2e_relevant excludes frontend/**, so a frontend-only PR skips e2e entirely. The only other workflow triggered on frontend/** is ci-frontend-lint.yaml, which runs biome ci src/ — a linter. It never builds.

Before this PR, e2e ran on every non-draft PR and built the frontend image, which does real work:

  • frontend.Dockerfile:44RUN bun install --frozen-lockfile --ignore-scripts
  • frontend.Dockerfile:50RUN bun run build

So a frontend-only change that breaks bun run build (bad import, TS/bundler error) or desyncs bun.lock from package.json is lint-clean and now merges green — it will next be caught by production-build.yaml on main, i.e. after merge.

The cost saving is real and worth having; the gap is that nothing replaces the build. Either keep frontend/** in e2e_relevant, or add a build-only job to the frontend lint workflow.

Comment thread tests/rig/cli.py
# gates (e.g. e2e-smoke: is the platform actually up?) does not hold, so
# running its dependents only yields noise against a half-up stack.
#
# `optional` cascades like any other dep: it governs whether a failure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 — groups.py's docstring now asserts the opposite of what this PR implements, and points at a TODO this PR deletes. [verified: read tests/rig/groups.py:222-228 in this worktree]

_validate_platform_groups_depend_on_gate's docstring still reads:

Note: runtime skip-on-gate-failure (not running dependents when the gate reports red) is NOT implemented yetcmd_run executes every runnable group unconditionally. Today this is moot because every dependent is optional: true (a placeholder) [...] add dep-failure tracking in cmd_run [...] see the TODO there.

All three claims are now false: this hunk implements the tracking, groups.yaml removes optional: true from e2e-workflow / e2e-api-deployment / e2e-prompt-studio, and the referenced TODO is gone — so the cross-reference dangles.

It's the docstring of the function that enforces the structural half of this invariant, so the next person reading it will conclude there is no runtime gating when there now is. Worth updating in the same PR that invalidates it.

Comment thread tests/e2e/etl/conftest.py Outdated
"UNSTRACT_MINIO_INTERNAL_URL", f"{scheme}://unstract-minio:9000"
)
client = minio.Minio(
endpoint, access_key=access_key, secret_key=secret_key, secure=False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — UNSTRACT_MINIO_SCHEME can't do what its comment says; the SonarCloud S5332 fix is cosmetic. [verified: read :57-65]

scheme (:59) feeds only internal_url (:61) — the endpoint handed to the workers. The test's own client is pinned secure=False (:64). Setting UNSTRACT_MINIO_SCHEME=https therefore tells the workers to use TLS while leaving the test client plaintext: the two halves disagree, and the "TLS-fronted deployment can override" the comment promises isn't reachable.

This matters mainly because the var was introduced to clear the Sonar clear-text-HTTP finding — the literal moved, the plaintext client didn't. Either derive secure= from scheme too, or drop the var and suppress the Sonar rule on a compose-internal test fixture, which is the honest description of what's happening.

Comment thread tests/README.md Outdated

### Hermetic LLM (`UNSTRACT_LLM_MOCK_RESPONSE`)

Execute-path e2e tests must not call a real provider, so `ComposeRuntime.up()` sets `UNSTRACT_LLM_MOCK_RESPONSE` (default `MOCK_LLM_OK`) before boot. The test overlay forwards it into the workers, and `unstract.sdk1.llm` passes it to litellm as `mock_response`: litellm returns the string verbatim with fixed usage (10 prompt / 20 completion / 30 total), so both the answer and the token counts are exact-assertable. Sentinels like `litellm.RateLimitError` force error paths. Unset (the production default) the hook is a no-op.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — two things this section states unconditionally hold only for the non-streaming path. [verified: test_connection read at llm.py:288-303; streaming usage measured against the pinned litellm 1.90.3]

1. 10 / 20 / 30 is completion-only. With stream=True, stream_options={"include_usage": True} — which is exactly what stream_complete passes (llm.py:554-558, and it is hooked at :546) — litellm returns 8 prompt / 3 completion / 11 total, not 10/20/30. The content assembles correctly; only the usage differs. Since the README presents 10/20/30 as a property of the mock and test_mock_response.py:77 calls it "litellm's defaults, asserted verbatim by the e2e tests", the next person to write a streaming assertion will trust it and get a confusing failure. No streaming test exists to pin it (the file's 7 tests all use the non-streaming path).

2. While the hatch is active, LLM adapter test-connection cannot pass. test_connection() calls self.complete() (llm.py:291), which now injects the mock, then regex-matches the response for "chennai" (:294). Under the rig's default MOCK_LLM_OK this never matches, so it raises the misleading "The credentials was valid however a sane response was not obtained" (:299-303).

Nothing today breaks — but adapter-register-llm is a tracked critical path currently listed as a gap, and this makes it unclosable in the e2e tier while the mock is set platform-wide. The PR description mentions test_connection; the README, which is the durable doc, doesn't. Worth adding beside the existing (good) note about embeddings being unhooked.

Comment thread tests/e2e/api_deployment/conftest.py Outdated

@pytest.fixture(scope="session")
def api_deployment(provisioned_workflow: ProvisionedWorkflow) -> ApiDeployment:
"""Deploy the provisioned workflow as an API, once for the whole group."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 — "once for the whole group" isn't what happens; provisioning runs once per xdist worker. [verified: tests/rig/groups.py:49 defaults parallel: bool = True, no e2e group in groups.yaml overrides it, and _pytest_command appends -n auto; the PR's own CI run shows these 3 tests on gw0/gw1/gw2]

Under -n auto each worker gets its own pytest session, so session-scoped fixtures run per worker — meaning provisioned_workflow and api_deployment execute up to 3 times for this group's 3 tests. Each cycle creates 4 adapters, a Prompt Studio project, an export, a workflow and a deployment against the shared platform.

It passes today (20.8s in CI), so this is not urgent. But the docstring is misleading, and 3x provisioning against one stack while each test holds a 300s poll is a plausible contributor to future timeout flakes under load. Either fix the docstring, or pin these groups to parallel: false given they already share a session-scoped platform.

@jaseemjaskp jaseemjaskp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR Review — hermetic LLM mock hook + execute-path e2e coverage

Ran the full PR Review Toolkit (code review, silent-failure hunt, type design, test coverage, comment analysis, simplification) plus independent verification of each claim against the code.

Overall this is unusually disciplined test infrastructure. The proof: marker ratchet is the strongest thing here: because passed_critical_path_ids counts only passing marked tests, a skipped or blocked test degrades to a reported gap rather than a silent green — that safety net catches most of what would otherwise be serious findings. The SDK hook itself is well-built: env-gated, never clobbers an explicit mock_response, applied uniformly at all four completion sites so there's no half-mocked path, value never logged. The e2e tests assert real system outputs (the answer arriving through the API, landing in the object store, written back to the Prompt Studio row) rather than asserting on the mock. No tautological tests found.

Three blocking findings:

  1. test_api_deployment_fan_out.py names its own false-pass mode in the docstring and then doesn't guard against it — and critical_paths.yaml retires a declared gap on that test's word.
  2. _blocked_result never reaches the CI report. cmd_report rebuilds from junit only, and the blocked branch writes none — so the new ⏭️ status and blocked_by note are invisible in the sticky PR comment, which is the only place they'd be read.
  3. e2e-api-deployment can be SIGKILLed before any test reports why: 3 × 300s poll budget against a 630s subprocess cap.

The rest are low/nit — mostly skip-paths that read green outside CI, a few comments whose stated reason is wrong even where the conclusion is right, and some duplication worth collapsing now that there are four e2e packages.

Findings verified against the code; already-posted review threads were cross-checked and excluded.

}


@pytest.mark.critical_path("workflow-execution-fan-out")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[blocking] The fan-out test cannot fail for the one thing it exists to prove — and it retires a declared gap on that basis.

The module docstring names the false-pass mode outright (lines 3-5): "at the default of 1 every file lands in a single batch and this would pass while proving nothing." Nothing in the test then checks that premise. All four assertions hold identically for a fully serialised single-batch run:

  • by_name / sorted(by_name) == sorted(_DOCUMENTS) — 3 results either way
  • _assert_totals_rejoined (:57) — total_files/successful_files/failed_files are the same serially
  • _assert_recorded_per_file (:71) — len(rows) == 3 regardless of batching

Failure scenario: the overlay uses MAX_PARALLEL_FILE_BATCHES=${MAX_PARALLEL_FILE_BATCHES:-3}, so any of — a shell/CI export of 1 (and backend/sample.env:218 ships MAX_PARALLEL_FILE_BATCHES=1), a dropped overlay (tests/rig/runtime.py applies it only if COMPOSE_OVERLAY.exists()), or an org-level Configuration row (backend/configuration/enums.py:56, which overrides per-org) — silently reduces this to a slower duplicate of test_api_deployment_run. It still reports green.

This matters more than the usual "weak assertion" because tests/critical_paths.yaml flips workflow-execution-fan-out from covered_by: [] # gap to covered_by: [e2e-api-deployment] on the strength of this test. A degraded overlay silently retires a declared gap.

Suggested fix: assert the precondition rather than assuming it. Either read the effective value back (backend/configuration/internal_views.py serves exactly this key to workers) and fail if < 2, or assert observable parallelism in _assert_recorded_per_file — distinct batch/task identifiers on the per-file rows, or overlapping [created_at, modified_at] windows.

Comment thread tests/rig/cli.py
# ── execution helpers ─────────────────────────────────────────────────────────


def _blocked_result(group: GroupDefinition, blocked_by: tuple[str, ...]) -> GroupResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[blocking] The new blocked status never reaches the report CI actually publishes.

Distinct from the exit-semantics thread below — this is about persistence, not gating.

_blocked_result is appended to the in-memory group_results only. The blocked branch continues before _execute_group, so no junit.xml and no exit.txt are written for the group. cmd_report (cli.py:316-321) rebuilds its group_results exclusively from parse_junit(name, tier, reports_dir) and drops anything returning None. The CI report job re-runs rig report over the downloaded artifacts (.github/workflows/ci-test.yaml:284-296).

Failure scenario: e2e-smoke goes red; e2e-etl / e2e-api-deployment / e2e-workflow / e2e-prompt-studio are correctly skipped and print [rig] SKIP … on the runner's stdout — but the sticky PR comment shows those groups simply missing, with no ⏭️ row and no "blocked by e2e-smoke" note. The blocked_by annotation added at reporting.py:251-263 and the ⏭️ icon are unreachable in CI; only the local run's stdout ever renders them. The feature is invisible exactly where it was meant to be read.

Suggested fix: in the blocked branch write a synthetic junit — _write_synthetic_junit already exists at cli.py:775 — with a skipped testcase plus exit.txt, or persist blocked_by into summary.json and have cmd_report merge it back over the junit-derived results.

Comment thread tests/groups.yaml
depends_on: [e2e-smoke]
optional: true

e2e-api-deployment:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[blocking] e2e-api-deployment's poll budget exceeds its group timeout, so a slow runner gets SIGKILLed before any test can report why.

The group inherits the 600s default (tests/rig/groups.py:48). tests/rig/cli.py:967 hard-caps the subprocess at timeout + 30 = 630s. But _POLL_TIMEOUT_SECONDS = 300 (tests/e2e/api_deployment/conftest.py:19) applies per test, and this group now has three of them (test_api_deployment_run, test_api_deployment_async, test_api_deployment_fan_out) — a worst case of ~900s of polling plus session provisioning.

Failure scenario: a loaded runner where the callback worker lags. _spawn kills the group at 630s and returns 124 before any test reaches its own pytest.fail(f"result never delivered within…"). No junit is written, so the group shows as a bare red row with no per-test detail, and all four critical paths it now claims (api-deployment-run, usage-token-tracking, callback-result-delivery, workflow-execution-fan-out) report as gaps/regressions with no clue which execution stalled.

Suggested fix: give e2e-api-deployment an explicit timeout_seconds above the summed poll budget (e.g. 1200, matching e2e-smoke), or drop _POLL_TIMEOUT_SECONDS to ~150s so 3 × poll + overhead fits inside 600s.

Comment thread tests/rig/runtime.py Outdated
def up(self) -> PlatformEndpoints:
if shutil.which("docker") is None:
raise RuntimeError("ComposeRuntime requires the `docker` CLI on PATH")
# setdefault so a CI/dev override wins.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] Only ComposeRuntime sets the mock var, and setdefault treats an exported empty string as "already set" — both routes end in the whole execute-path suite skipping green.

Two independent holes in the same two lines:

  1. Non-compose runtimes never set it. LocalRuntime.up() (:119) and TestcontainersRuntime.up() don't touch LLM_MOCK_RESPONSE_ENV, and _default_runtime_name() (:257) returns testcontainers whenever CI is unset. So for a developer running the e2e tier locally, or anyone on --runtime local against a dev stack, llm_mock_response (tests/e2e/conftest.py:91-96) skips and all six new execute-path tests skip. Every e2e group exits 0 → ✅ pass, and the five new critical paths quietly become gaps. CI is protected by --fail-on-critical-gap (ci-test.yaml:216); a local python -m tests.rig run --tier e2e is not, and reads fully green with zero coverage.

  2. setdefault accepts "". UNSTRACT_LLM_MOCK_RESPONSE= counts as already-overridden. The empty string flows into the workers via docker-compose.test.yaml:27/31 (${UNSTRACT_LLM_MOCK_RESPONSE:-}), where _inject_mock_response reads it as falsy and skips injection — and llm_mock_response reads it as falsy and skips every test. One stale export UNSTRACT_LLM_MOCK_RESPONSE= in a shell profile silently deletes the entire execute-path suite while the run stays green.

Suggested fix: if not os.environ.get(LLM_MOCK_RESPONSE_ENV): os.environ[LLM_MOCK_RESPONSE_ENV] = DEFAULT_LLM_MOCK_RESPONSE, hoisted into cmd_run for any runtime when platform groups are selected. And make the fixture fail rather than skip when the rig is driving — cmd_run already stamps UNSTRACT_RIG_SESSION_ID (cli.py:752) for precisely this distinction.

Comment thread tests/rig/cli.py Outdated
if result is not None:
group_results.append(result)
# Tracked regardless of `optional` — see the cascade note above.
if exit_code not in _NON_FAILING_PYTEST_EXIT_CODES or (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] A dependency that collected zero tests does not block its dependents.

_NON_FAILING_PYTEST_EXIT_CODES = (0, 5) (cli.py:52), so pytest's exit 5 — "no tests collected" — never lands in failed_groups. The gate here is e2e-smoke, whose entire job is answering "is the platform actually up?". A path typo in groups.yaml, a --marker filter that matches nothing, or a renamed test file turns that question into unanswered, and the new cascade treats it as yes — every dependent then runs against a platform nothing verified.

This is the same reasoning the sibling comment at cli.py:690-694 already applies to coverage attestation ("empty" … must not attest); the gating path just didn't inherit it. Either way the choice deserves to be explicit — the comment block above reasons only about red deps, not empty ones.

Suggested fix: treat exit 5 as failed for the purposes of failed_groups when the group is something else depends_on, and pin whichever semantics you pick with a cmd_run test (stub _execute_group to return (None, 5) for smoke, assert dependents do/don't run).

Comment thread tests/critical_paths.yaml Outdated
@@ -66,6 +68,7 @@ paths:
- id: prompt-studio-fetch-response
description: "Prompt Studio: create project, add prompt, run single-pass, get response."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] This path now attests a variant its covering test explicitly excludes.

The description says "run single-pass, get response", and the path was just flipped to proof: marker against test_prompt_studio_fetch_response.py. That test posts to fetch_response/ (not the single-pass route) and reads outputs with "is_single_pass_extract": "false" (tests/e2e/prompt_studio/test_prompt_studio_fetch_response.py:92). The registry is now recording coverage for something the test deliberately opts out of.

Suggest "Prompt Studio: create project, add prompt, run a prompt, get response.", or keep single-pass as a separate, still-uncovered path.

Same class of overstatement, milder, at :44 — workflow-create-execute ends "poll, fetch result", but test_workflow_execute.py's own docstring says it "asserts execution status rather than the answer, which a manual execute never exposes over HTTP".

Comment thread tests/README.md
│ ├── api_deployment/ # API deployment e2e: sync, async, fan-out
│ ├── etl/ # ETL pipeline e2e (MinIO source + destination)
│ ├── prompt_studio/ # Prompt Studio fetch-response e2e
│ └── hurl/ # (future) hurl-based HTTP suites

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] "sync" overstates what's covered — all three api_deployment tests dispatch async.

Every module in tests/e2e/api_deployment/ calls dispatch_async(...) with timeout=0. test_api_deployment_run.py's own docstring says so explicitly: "Dispatches async and polls the status endpoint … which the sync POST cannot reliably wait out." The conftest comment at :15-18 says the same.

Suggest: # API deployment e2e: run, callback delivery, fan-out (all async).

from types import ModuleType

# Stub python-magic so importing LLM does not depend on libmagic.
sys.modules.setdefault("magic", ModuleType("magic"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The magic stub is inserted into sys.modules and never removed.

sys.modules.setdefault("magic", ModuleType("magic")) runs inside an lru_cached loader, so it persists for the rest of the process. If magic is installed but not yet imported when this module happens to run first, the empty stub shadows the real library for every subsequent sdk1 test — an ordering-dependent failure in an unrelated test, which is a nasty one to chase.

Suggest a fixture that pops the key it inserted (and only if it inserted it), or restrict the stub to the import itself via unittest.mock.patch.dict(sys.modules, ...).

Comment thread tests/e2e/etl/conftest.py
"""Where the test and the workers each reach the object store."""

client: object
bucket: str

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] client: object erases the only interface this fixture exists to provide.

Every consumer immediately calls methods object doesn't have — store.client.put_object(...), .list_objects(...), .get_object(...) at tests/e2e/etl/test_pipeline_etl_execute.py:51,98,103. No autocomplete, no arity checking, and mypy would reject all three call sites if it were turned on for tests/. The dataclass is the natural place to publish "this is a minio.Minio".

Two lines, and it keeps the runtime importorskip:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from minio import Minio

@dataclass(frozen=True)
class MinioFixture:
    client: "Minio"

Also in this block: endpoint is passed to minio.Minio raw, so someone following the neighbouring _INTERNAL_URL convention and exporting UNSTRACT_MINIO_ENDPOINT=http://localhost:9000 gets a ValueError at fixture setup rather than a skip — the one env var here that must not carry a scheme sits directly beside one that must.

Comment thread tests/rig/cli.py Outdated
group = manifest.get(name)
blocked_by = tuple(sorted(manifest.transitive_deps(name) & failed_groups))
if blocked_by:
print(f"\n[rig] SKIP {name} (dependency failed: {', '.join(blocked_by)})")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] "dependency failed" isn't always true of the group it names.

The skip branch adds blocked groups to failed_groups, so a third-level group's blocked_by can name a dependency that was itself only blocked, never run. Given A red → B blocked → C blocked, the console prints [rig] SKIP C (dependency failed: B) — but B didn't fail, it was skipped. Same wording in _blocked_result's docstring and reporting.py:47.

Suggest "dependency failed or was blocked" in all three.

Separately, the comment above (:448-450) credits the weaker of the two gating mechanisms: a blocked group's path reports as a gap only when it wasn't green on the baseline — if it was, critical_paths.py:191 classifies it as a regression, and regressions fail unconditionally (cli.py:579-586) whereas gaps need --fail-on-critical-gap (cli.py:602). Worth saying "…reports as a gap (or a regression if it was green on the baseline) and gates accordingly."

One more, on duplication this PR surfaced: the CSRF-header helper now exists in seven near-copies across tests/e2e/ — and they've already drifted (tests/e2e/conftest.py:122 merges caller headers; tests/e2e/etl/conftest.py:81 silently discards them). A requests.Session subclass that stamps the header in request() would delete all seven call-site helpers.

@muhammad-ali-e muhammad-ali-e left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM
Did a manual review since already done couple of AI reviews


pytestmark = [pytest.mark.e2e, pytest.mark.critical]

_TERMINAL = {"COMPLETED", "ERROR", "STOPPED"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: can we set the TERMINAL statuses in a common place like /unstract/unstract.

Comment thread tests/rig/reporting.py Outdated
@property
def status(self) -> ResultStatus:
if self.blocked_by:
return "blocked"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: would be better making these terms "blocked", "pass" etc are a constant.

Comment thread tests/rig/reporting.py Outdated
@property
def status(self) -> ResultStatus:
if self.blocked_by:
return "blocked"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: would be better making these terms "blocked", "pass" etc are a constant.

chandrasekharan-zipstack and others added 3 commits July 21, 2026 11:22
`report` needs `changes` but never consulted its result. A failed filter job
(paths-filter error, checkout failure, runner death) skips `test`, which the
gate reads as a legitimate path-filter skip and reports success with no unit
or integration tests having run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
Rig:
- Blocked groups now persist a synthetic junit carrying `rig-blocked-by`, so
  the cascade survives `rig report`, which rebuilds results from junit alone.
  Without it a blocked group parsed as a zero-count pass and falsely attested
  critical-path coverage.
- GroupResult rejects a blocked row with non-zero counters.
- Report statuses are module constants instead of scattered literals.
- An exit-5 (no tests collected) group that others depend on now counts as
  failed, so dependents are blocked rather than run against nothing.
- The LLM mock default moves to cmd_run so it applies to every runtime.
- groups.py docstring no longer claims runtime gating is unimplemented.

E2E:
- Shared `wait_for_execution` helper replaces three near-identical polls; a
  CsrfSession subclass stamps X-CSRFToken so per-call helpers can't drift.
- api_deployment poll: sleeps on every iteration, captures exceptions as
  evidence, treats 406 as a lost-200 acknowledge rather than a poll assert,
  and surfaces non-422 statuses as themselves. Timeout drops to 150s so the
  group's three tests fit one 600s budget.
- MinIO scheme drives both the workers' URL and the client's TLS flag.
- Missing rig-provisioned prerequisites fail instead of silently skipping.

Core/SDK:
- `ExecutionStatus.terminal_statuses()` names the terminal set.
- Mock-response tests cover the empty-string no-op and assert every
  completion path still calls the injection hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
The e2e image build is the only CI check that runs `bun install` and
`bun run build`; ci-frontend-lint only runs biome. Excluding frontend/**
from e2e_relevant let a broken build or a desynced bun.lock merge green
and surface on main via production-build.yaml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 16.7
e2e-coowners e2e 1 0 0 0 1.7
e2e-etl e2e 1 0 0 0 8.2
e2e-login e2e 2 0 0 0 1.2
e2e-prompt-studio e2e 1 0 0 0 4.6
e2e-smoke e2e 2 0 0 0 1.1
e2e-workflow e2e 1 0 0 0 16.3
integration-backend integration 124 0 0 27 57.3
integration-connectors integration 1 0 0 7 6.6
unit-backend unit 151 0 0 0 23.1
unit-connectors unit 63 0 0 0 10.1
unit-core unit 27 0 0 0 1.3
unit-platform-service unit 15 0 0 0 2.8
unit-rig unit 76 0 0 0 4.5
unit-sdk1 unit 480 0 0 0 24.4
unit-workers unit 723 0 0 0 48.7
TOTAL 1671 0 0 34 228.6

Critical paths

✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • workflow-execution-fan-out — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

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.

5 participants