Skip to content

UN-3636 [MISC] Drop the ENVIRONMENT gate on the LLM mock#2191

Merged
chandrasekharan-zipstack merged 2 commits into
feat/rig-extra-manifestsfrom
feat/un-3636-drop-environment-gate
Jul 22, 2026
Merged

UN-3636 [MISC] Drop the ENVIRONMENT gate on the LLM mock#2191
chandrasekharan-zipstack merged 2 commits into
feat/rig-extra-manifestsfrom
feat/un-3636-drop-environment-gate

Conversation

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor

Follow-up to #2170, removing a guard that PR added.

Why

The gate required ENVIRONMENT ∈ {test, development} on top of UNSTRACT_LLM_MOCK_RESPONSE before the SDK would mock a completion. It was added for this review comment — the hazard being a worker env block copied out of the test overlay into a real deployment, where litellm's synthetic 10/20/30 token usage would flow into the usage tables indistinguishable from real spend.

It does not defend that case:

  • The gate was written into that same overlay block, next to the mock var, so a copy carries the gate along with it.
  • docker/docker-compose.yaml sets ENVIRONMENT=development on both workers that run the injection, so any deployment derived from base compose — which is how Unstract is self-hosted — satisfied the gate anyway.

That leaves one case it genuinely catches: the mock var set alone somewhere that sets no ENVIRONMENT at all. That holds by accident (nothing sets the variable in k8s) rather than by design, and it costs a dependency on a variable nothing else in the codebase reads — 16 of its 20 declarations across the compose files are dead.

What still guards the hatch

Unchanged: it is off unless someone sets UNSTRACT_LLM_MOCK_RESPONSE, and it logs a warning once per process while active.

The defence worth having is making mocked spend distinguishable downstream — an is_mock marker on the usage record, which config checks can't be copy-pasted around. Not in this PR.

Changes

  • unstract/sdk1/src/unstract/sdk1/llm.py — drop _ENVIRONMENT_ENV, _MOCK_ALLOWED_ENVIRONMENTS, _warn_mock_refused, and the gate
  • unstract/sdk1/tests/test_mock_response.py — drop the 4 gate tests and the autouse env fixture (9 tests remain, all passing)
  • tests/compose/docker-compose.test.yaml — drop the two ENVIRONMENT=test lines added for the gate
  • tests/README.md — record why it was removed, so it doesn't get re-added

🤖 Generated with Claude Code

It did not defend the case it was added for. The threat was a worker env block
copied out of the test overlay into a real deployment, but the gate was written
into that same block, so a copy carries it. Base compose also sets
ENVIRONMENT=development on both workers that run the injection, so any
deployment derived from it satisfied the gate regardless. That left one real
case -- the mock var set alone somewhere that sets no ENVIRONMENT at all --
which holds by accident rather than design, in exchange for depending on a
variable nothing else in the codebase reads.

What actually guards the hatch is unchanged: it is off unless someone sets
UNSTRACT_LLM_MOCK_RESPONSE, and it warns once per process while active. Making
mocked spend distinguishable downstream is the defence worth having, and it
belongs on the usage record rather than in a config check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

LLM mocking no longer depends on ENVIRONMENT. Setting UNSTRACT_LLM_MOCK_RESPONSE enables mock response injection, while Docker Compose configurations and tests remove explicit environment gates.

Changes

LLM mocking behavior

Layer / File(s) Summary
Mock response injection and validation
unstract/sdk1/src/unstract/sdk1/llm.py, unstract/sdk1/tests/test_mock_response.py, tests/README.md
Mock responses are injected when UNSTRACT_LLM_MOCK_RESPONSE is set, with one active warning per process; environment-refusal tests and documentation are removed.
Compose environment wiring
docker/docker-compose.yaml, tests/compose/docker-compose.test.yaml
Explicit ENVIRONMENT overrides are removed, and the test worker configuration uses UNSTRACT_LLM_MOCK_RESPONSE.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers the rationale and changes, but it omits most required template sections such as How, breakage, testing, and checklist. Add the missing template sections, especially How, breakage impact, Database Migrations, Env Config, testing notes, related issues, and the checklist.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: removing the ENVIRONMENT gate from the LLM mock.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/un-3636-drop-environment-gate

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.

Nothing reads it: no service, worker, frontend or plugin looks the variable
up, and the one consumer it ever had — the LLM mock gate — was removed in the
previous commit. Dropping it everywhere keeps a dead knob from looking
load-bearing to the next reader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ryg9chVDJQggCybpq3YoY3
@chandrasekharan-zipstack
chandrasekharan-zipstack marked this pull request as ready for review July 21, 2026 11:53
@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR removes the environment gate from the LLM mock path. The main changes are:

  • UNSTRACT_LLM_MOCK_RESPONSE now controls SDK mock injection by itself.
  • The old environment refusal helper and related tests were removed.
  • Compose files no longer set ENVIRONMENT for base or test services.
  • Test documentation now describes the env-var-only mock behavior.

Confidence Score: 4/5

The LLM mock path needs a production-safe guard before merging.

  • A real worker can now mock completions when UNSTRACT_LLM_MOCK_RESPONSE is set.
  • Mocked LiteLLM usage can flow into normal usage records without a marker.
  • The compose ENVIRONMENT removals appear safe because no remaining code path reads that variable.

unstract/sdk1/src/unstract/sdk1/llm.py

Security Review

A real worker with a stray non-empty UNSTRACT_LLM_MOCK_RESPONSE can now bypass provider calls and record mocked usage without a mock marker.

Important Files Changed

Filename Overview
unstract/sdk1/src/unstract/sdk1/llm.py Removes the ENVIRONMENT allow-list before LiteLLM mock injection.
unstract/sdk1/tests/test_mock_response.py Removes tests for environment-based refusal while keeping unset, empty, set, override, warning, and completion-path checks.
docker/docker-compose.yaml Removes ENVIRONMENT=development from compose services.
tests/compose/docker-compose.test.yaml Removes ENVIRONMENT=test from test services while keeping mock-response forwarding for the main execute-path workers.
tests/README.md Updates the test documentation for the removed gate and remaining mock behavior.

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
unstract/sdk1/src/unstract/sdk1/llm.py:55-56
**Mocked Completions Reach Production**

When a real worker has a non-empty `UNSTRACT_LLM_MOCK_RESPONSE`, this path now injects `mock_response` without any runtime gate. All SDK completion paths can then return canned LiteLLM output and persist synthetic token usage as normal usage records, with no in-band mock marker for billing or dashboards to filter.

Reviews (1): Last reviewed commit: "UN-3636 [MISC] Drop the unread ENVIRONME..." | Re-trigger Greptile

Comment thread unstract/sdk1/src/unstract/sdk1/llm.py
@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.8
e2e-coowners e2e 1 0 0 0 1.9
e2e-etl e2e 1 0 0 0 8.4
e2e-login e2e 2 0 0 0 1.3
e2e-prompt-studio e2e 1 0 0 0 4.9
e2e-smoke e2e 2 0 0 0 1.1
e2e-workflow e2e 1 0 0 0 16.8
integration-backend integration 124 0 0 27 68.3
integration-connectors integration 1 0 0 7 8.1
unit-backend unit 160 0 0 0 22.1
unit-connectors unit 63 0 0 0 9.7
unit-core unit 27 0 0 0 1.3
unit-platform-service unit 15 0 0 0 2.6
unit-rig unit 76 0 0 0 4.5
unit-sdk1 unit 480 0 0 0 24.8
unit-workers unit 723 0 0 0 47.2
TOTAL 1680 0 0 34 239.8

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ 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
  • callback-result-delivery — covered by e2e-api-deployment

@chandrasekharan-zipstack chandrasekharan-zipstack changed the title UN-3636 [FIX] Drop the ENVIRONMENT gate on the LLM mock UN-3636 [MISC] Drop the ENVIRONMENT gate on the LLM mock Jul 21, 2026
@jaseemjaskp
jaseemjaskp self-requested a review July 22, 2026 05:43
@chandrasekharan-zipstack
chandrasekharan-zipstack changed the base branch from main to feat/rig-extra-manifests July 22, 2026 10:39
@chandrasekharan-zipstack
chandrasekharan-zipstack merged commit da2a5ce into feat/rig-extra-manifests Jul 22, 2026
15 checks passed
@chandrasekharan-zipstack
chandrasekharan-zipstack deleted the feat/un-3636-drop-environment-gate branch July 22, 2026 10:39
chandrasekharan-zipstack added a commit that referenced this pull request Jul 22, 2026
…TS (#2188)

* UN-3636 [FEAT] Merge overlay manifests via UNSTRACT_RIG_EXTRA_MANIFESTS

load_groups() now merges extra group manifests listed in the env var
(os.pathsep-separated, REPO_ROOT-relative) onto the base tests/groups.yaml
before validation, so cross-manifest depends_on and the platform-gate
invariant are checked over the union. Name collisions are an error.

Lets a downstream repo (the cloud build) contribute its own test groups by
copying a groups.cloud.yaml into the merged tree, without editing the OSS
manifest.

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

* UN-3636 [FIX] Skip org lookup when no org in context

UserContext.get_organization() ran Organization.objects.get() even with no
org id in StateStore (import time, or management commands with no request),
catching only DoesNotExist/ProgrammingError — a DB-less/unmigrated setup hit
an uncaught OperationalError. Short-circuit when there's no org id: no query,
so serializers/managers that reference org-scoped querysets at class-def can
be imported during DB-free test collection.

* UN-3636 [FIX] Scope overlay manifests to the default manifest

Address review feedback on the UNSTRACT_RIG_EXTRA_MANIFESTS overlay:

- Overlays now apply only when loading the default manifest, so an
  explicit `load_groups(path)` (test fixture, ad-hoc manifest) can no
  longer absorb a downstream repo's ambient overlay.
- `_merge_manifest` returns the merged defaults so an overlay can rename
  `platform_gate_group` instead of having it silently ignored.
- A bad path in the env var raises a ValueError naming the variable
  rather than a bare FileNotFoundError/IsADirectoryError.
- Extract `_load_manifest_dict` to single-source manifest parsing and its
  error message.
- Tests: drive the real default-manifest path; cover overlay isolation,
  overlay defaults, malformed overlay, and a missing overlay path.
- Pin the truthy branch of UserContext.get_organization so inverting the
  guard can't pass unnoticed.

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

* UN-3636 [FIX] Stop re-running cross-tier deps in every tier leg

`--tier X` expanded each selected group's `depends_on` transitively, with
no tier bound. `integration-workflow-execution` and `e2e-smoke` both
declare `depends_on: [unit-sdk1, unit-workers]`, so those two unit groups
ran again in the integration leg and a third time in the e2e leg.

Tiers run as separate CI legs and the unit leg already covers them, so
dep expansion is now bounded to the requested tier. Explicitly named
groups are never dropped, and intra-tier deps (e2e-smoke -> e2e-login)
still expand and order as before. Unrun deps do not weaken gating:
`blocked_by` intersects with groups that failed in the same run.

Measured on the last main run: ~88s of unit-workers and ~48s of
unit-sdk1 re-executed per run. On the cloud CI runner the same
duplication costs ~350s.

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

* UN-3636 [MISC] Drop the ENVIRONMENT gate on the LLM mock (#2191)

* UN-3636 [FIX] Drop the ENVIRONMENT gate on the LLM mock

It did not defend the case it was added for. The threat was a worker env block
copied out of the test overlay into a real deployment, but the gate was written
into that same block, so a copy carries it. Base compose also sets
ENVIRONMENT=development on both workers that run the injection, so any
deployment derived from it satisfied the gate regardless. That left one real
case -- the mock var set alone somewhere that sets no ENVIRONMENT at all --
which holds by accident rather than design, in exchange for depending on a
variable nothing else in the codebase reads.

What actually guards the hatch is unchanged: it is off unless someone sets
UNSTRACT_LLM_MOCK_RESPONSE, and it warns once per process while active. Making
mocked spend distinguishable downstream is the defence worth having, and it
belongs on the usage record rather than in a config check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3636 [MISC] Drop the unread ENVIRONMENT variable from compose

Nothing reads it: no service, worker, frontend or plugin looks the variable
up, and the one consumer it ever had — the LLM mock gate — was removed in the
previous commit. Dropping it everywhere keeps a dead knob from looking
load-bearing to the next reader.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3636 [MISC] Tighten code comments

Drop lines that restate the code, trim session-specific detail, and merge
comments that duplicated each other across a module and its test.

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

* UN-3636 [FIX] Gate overlay merging on an omitted path, not its value

`load_groups(DEFAULT_MANIFEST)` merged overlays even though the caller named a
manifest explicitly, because the check compared path values. Path equality is
also spelling-sensitive, so the same file relative and absolute behaved
differently. Key on `path is None` instead: an explicit path loads exactly what
it names.

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

* UN-3636 [FIX] Stop ide_prompt_complete tests from hitting the network

TestIdePromptComplete drives the full success path, which reaches
client_plugin_registry.get_client_plugin("subscription_usage"). In OSS no
such plugin is installed, so the lookup returns None instantly. In a tree
with the cloud plugins copied in, it resolves to a real plugin that POSTs
to the backend; with no backend running the call only fails after a
multi-second connect timeout, and _track_subscription_usage swallows the
error so the tests still pass. That accounted for ~190s of the cloud
unit-workers run.

The file already declared _PATCH_GET_PLUGIN but never applied it outside
the dedicated subscription-usage classes. Apply it as a class-scoped
fixture so the lookup is pinned to the OSS answer.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant