Skip to content

test(ci): exercise the public API on a bare install (#189) - #211

Merged
ptimizeroracle merged 1 commit into
mainfrom
fix/189-bare-install-smoke
Aug 3, 2026
Merged

ptimizeroracle merged 1 commit into
mainfrom
fix/189-bare-install-smoke

Conversation

@ptimizeroracle

@ptimizeroracle ptimizeroracle commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Closes #189.

Why

Every CI job installs the dev environment (uv sync --all-extras), so two whole classes of bug are invisible to it, and both shipped in 1.11.0:

Bug Why CI could not see it
enrich(polars_df, …) → ModuleNotFoundError: pyarrow --all-extras installs pyarrow, masking that it is not a core dependency
enrich(schema=…) → Either dataframe or source_path must be provided test_enrich.py stubs Pipeline.execute, so the loader stage never ran

The existing Install Profile jobs verify the package imports. They do not verify it works.

What

scripts/smoke_bare_install.py — does what a user does: install the wheel, call enrich(), look at the DataFrame. It talks to a local stub of the OpenAI chat-completions API, so it needs no key, costs nothing, and cannot flake on the network. Covers enrich() with a pandas frame, a Polars frame, a CSV path, and schema= structured output.

It asserts on the values, not the shape: a run where every row failed still has the right row count and column names, which is exactly how the original bug looked green.

New bare-install-smoke CI job — builds the wheel, installs it into a clean venv with --only-binary=:all: and no extras, runs the script. Added to build's needs.

tests/unit/test_enrich.py grows a TestEnrichEndToEnd class that patches the client factory instead of Pipeline.execute, so the loader, prompt, invocation, parser, and output-quality guard all run for real — closing the gap the issue calls out.

Verification

  • Script run against a bare pip install ondine==1.11.2 from PyPI in a clean venv (no pyarrow present): all four checks pass.
  • Mutation-tested both ways — the script fails when the stub returns a wrong answer, and all four new unit tests fail when the stub client raises. Neither passes vacuously.
  • Full unit suite: 1115 passed.

Two deliberate calls

[all] is not smoke-tested. The bug class this job exists for is only visible in the bare leg — every extra installed can only hide it. Install Profile (all) already proves [all] installs, and adding it here pulls torch via sentence-transformers for no extra detection power. The [mcp] leg is kept because it is cheap and catches extras-only import breakage.

The stub is reached via OPENAI_BASE_URL, not a parameter, because enrich() has no base_url — that is #208. If that gap closes, this should pass it explicitly; there is a comment in the script saying so.

Found in passing

#210 — the unit suite has a pre-existing flake: test_mcp_server.py starts background threads that outlive their tests, and a straggler litellm call can land inside another test's patch window. Hit it once during this work, filed rather than folded in.

ai assistance: i directed this work with help from claude code.

Summary by CodeRabbit

  • Tests

    • Added coverage for enrichment with pandas, Polars, CSV files, and structured output schemas.
    • Added validation for synchronous and asynchronous enrichment workflows.
    • Added smoke testing for fresh installations with and without optional features.
  • Chores

    • Updated continuous integration checks to run installation smoke tests before builds complete.

Every CI job installs the dev environment, so two whole classes of bug are
invisible to it: a dependency declared only in an extra, and a code path the
unit tests reach only through a mock. Both shipped in 1.11.0.

Adds scripts/smoke_bare_install.py, which does what a user does — install the
wheel, call enrich(), check the DataFrame — against a local stub of the
OpenAI chat-completions API, so it needs no key and cannot flake. It covers
pandas, Polars, a CSV path, and schema= structured output, and asserts on the
returned values rather than the frame's shape: a totally failed run still has
the right row count and column names.

The new CI job installs the built wheel into a clean venv with no extras and
runs it. The [mcp] leg catches extras-only import breakage; [all] is left out
deliberately — the Install Profile job already proves it installs, and extras
can only hide the bug class this job exists for.

Also grows tests/unit/test_enrich.py an end-to-end class that patches the
client factory instead of Pipeline.execute, so the loader, invocation, and
parser stages actually run. Verified by mutation: all four fail when the stub
client raises.

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

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Enrichment validation

Layer / File(s) Summary
Real pipeline integration tests
tests/unit/test_enrich.py
Added a deterministic LLMClient stub and end-to-end tests for pandas, Polars, CSV paths, and structured schemas.
Bare-install smoke execution
scripts/smoke_bare_install.py
Added a local OpenAI-compatible stub and smoke coverage for enrichment inputs, output types, row counts, and exact values.
CI installation gate
.github/workflows/ci.yml
Added release-wheel checks for bare and mcp installations. The build job now requires this smoke job.

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

Sequence Diagram(s)

sequenceDiagram
  participant SmokeTest
  participant Ondine
  participant LocalHTTPStub
  SmokeTest->>Ondine: Call enrich with dataframe, CSV, or schema
  Ondine->>LocalHTTPStub: Send OpenAI-compatible completion request
  LocalHTTPStub-->>Ondine: Return canned text, JSON-schema, or tool-call response
  Ondine-->>SmokeTest: Return enriched result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers the required API paths and end-to-end unit test, but the smoke job does not test the [all] extra profile [#189]. Extend the smoke job to install the built wheel with the [all] extra and run the public API smoke test [#189].
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the CI test and the bare-install public API coverage.
Out of Scope Changes check ✅ Passed The workflow, smoke script, and unit tests directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix/189-bare-install-smoke

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.

@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: 3

🤖 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 @.github/workflows/ci.yml:
- Around line 97-107: Update the extras loop in the CI workflow to add a
conditional MCP smoke step when extras is "[mcp]". After the existing
smoke_bare_install.py invocation, import ondine.mcp and call create_server()
using the smoke environment’s Python so the MCP entrypoint and FastMCP wiring
are exercised.
- Around line 68-73: Update the bare-install-smoke job to explicitly set
job-level contents permission to read, and configure its actions/checkout@v6
step with persist-credentials disabled; do not rely on repository-level token
defaults.

In `@scripts/smoke_bare_install.py`:
- Around line 190-197: Update the stub instrumentation used by the schema
scenario around _assert_answered to record whether the request includes
response_format.type equal to "json_schema" or a tools payload, then assert that
structured-output evidence before or alongside validating the sentiment result.
Ensure the enrich(schema=Sentiment) case fails when neither request field is
observed.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80cef3fd-b141-4ecd-b2a4-b94e87724eaa

📥 Commits

Reviewing files that changed from the base of the PR and between e472376 and 56f5f9f.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • scripts/smoke_bare_install.py
  • tests/unit/test_enrich.py

Comment thread .github/workflows/ci.yml
Comment on lines +68 to +73
bare-install-smoke:
name: Bare Install Smoke
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow context =="
if [ -f .github/workflows/ci.yml ]; then
  nl -ba .github/workflows/ci.yml | sed -n '1,140p'
else
  echo "missing .github/workflows/ci.yml"
fi

echo
echo "== checkout usages in workflows =="
rg -n "actions/checkout|persist-credentials|permissions:" .github/workflows || true

echo
echo "== git diff stat/name =="
git diff --stat || true
git diff -- .github/workflows/ci.yml 2>/dev/null | nl -ba | sed -n '1,220p' || true

Repository: ptimizeroracle/ondine

Length of output: 222


🌐 Web query:

actions/checkout v6 default persist-credentials true README

💡 Result:

The default value for the persist-credentials input in actions/checkout v6 is true [1][2]. When set to true (the default), the action configures the authentication token or SSH key with the local git configuration [1][2]. Starting with v6, this process was updated to improve security: instead of storing credentials directly in the.git/config file, they are stored in a separate file under $RUNNER_TEMP [1][3][4]. This token is automatically removed during the post-job cleanup [1][5]. You can set persist-credentials to false if you wish to opt out of this behavior [1][5].

Citations:


🌐 Web query:

GitHub Actions workflow syntax default GITHUB_TOKEN permissions jobs job-name permissions contents read

💡 Result:

In GitHub Actions, you can control the permissions granted to the GITHUB_TOKEN by using the permissions key in your workflow file [1][2]. When you define the permissions key, any permission not explicitly listed is set to none [2][3]. To set the permissions for a specific job, you define the permissions key within that job's configuration [2]. The following example grants only read access to the repository contents for a single job: jobs: job-name: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 Key principles regarding GITHUB_TOKEN permissions include: 1. Hierarchy: If permissions are defined at the workflow level, they apply to all jobs in the workflow [4]. If defined at the job level, they override any workflow-level permissions for that specific job [2][4]. 2. Default Settings: If no permissions are explicitly set, the GITHUB_TOKEN inherits the default settings defined at the repository or organization level [5][2]. 3. Principle of Least Privilege: It is a security best practice to use the permissions key to grant only the minimum access required for each job [1][2]. For more information, see the official documentation on workflow syntax for GitHub Actions [6][2].

Citations:


Restrict the job token and disable persisted checkout credentials.

This job builds and executes checked-out code. actions/checkout defaults persist-credentials to true, so later commands can use the checkout credential.

Set contents: read for this job and set persist-credentials: false. Do not rely on repository-level default token permissions.

Proposed fix
   bare-install-smoke:
     name: Bare Install Smoke
     runs-on: ubuntu-latest
+    permissions:
+      contents: read

     steps:
-    - uses: actions/checkout@v6
+    - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bare-install-smoke:
name: Bare Install Smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
bare-install-smoke:
name: Bare Install Smoke
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 73-73: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 68-111: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 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.yml around lines 68 - 73, Update the bare-install-smoke
job to explicitly set job-level contents permission to read, and configure its
actions/checkout@v6 step with persist-credentials disabled; do not rely on
repository-level token defaults.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml
Comment on lines +97 to +107
for extras in "" "[mcp]"; do
echo "::group::install ondine${extras:-} (no dev environment)"
rm -rf "${RUNNER_TEMP}/smokeenv"
python -m venv "${RUNNER_TEMP}/smokeenv"
"${RUNNER_TEMP}/smokeenv/bin/python" -m pip install --quiet --upgrade pip
# Wheels only, and install the wheel by path: a source build would
# need a toolchain users do not have, and resolving by name could
# quietly pull the last release from PyPI instead of this commit.
"${RUNNER_TEMP}/smokeenv/bin/python" -m pip install --quiet \
--only-binary=:all: "${wheel}${extras}"
"${RUNNER_TEMP}/smokeenv/bin/python" scripts/smoke_bare_install.py

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Find the public Ondine MCP import path before adding it to the smoke job.
rg -n -C 3 '\b(fastmcp|FastMCP)\b' ondine tests scripts

Repository: ptimizeroracle/ondine

Length of output: 6127


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- workflow outline ---\n'
wc -l .github/workflows/ci.yml
sed -n '80,115p' .github/workflows/ci.yml

printf '\n--- smoke script ---\n'
wc -l scripts/smoke_bare_install.py
sed -n '1,240p' scripts/smoke_bare_install.py

printf '\n--- mcp modules ---\n'
wc -l ondine/mcp/__init__.py ondine/mcp/server.py
sed -n '1,80p' ondine/mcp/__init__.py
sed -n '1,85p' ondine/mcp/server.py

printf '\n--- wheel metadata mentions mcp/fastmcp ---\n'
rg -n -C 2 -i '\b(mcp|fastmcp)\b' pyproject.toml setup.cfg setup.py ondine tests scripts 2>/dev/null || true

Repository: ptimizeroracle/ondine

Length of output: 250


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- workflow outline ---'
wc -l .github/workflows/ci.yml
sed -n '80,115p' .github/workflows/ci.yml

printf '%s\n' '\n--- smoke script ---'
wc -l scripts/smoke_bare_install.py
sed -n '1,240p' scripts/smoke_bare_install.py

printf '%s\n' '\n--- mcp modules ---'
wc -l ondine/mcp/__init__.py ondine/mcp/server.py
sed -n '1,80p' ondine/mcp/__init__.py
sed -n '1,85p' ondine/mcp/server.py

printf '%s\n' '\n--- wheel metadata mentions mcp/fastmcp ---'
rg -n -C 2 -i '\b(mcp|fastmcp)\b' pyproject.toml setup.cfg setup.py ondine tests scripts 2>/dev/null || true

Repository: ptimizeroracle/ondine

Length of output: 37231


Import and build the MCP server in the [mcp] iteration.

ondine[mcp] installs fastmcp, but smoke_bare_install.py does not exercise the MCP entrypoint, and ondine.mcp lazily defers create_server imports. Add a conditional MCP smoke step that imports ondine.mcp and calls create_server() so missing broken FastMCP wiring fails this CI leg.

🤖 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.yml around lines 97 - 107, Update the extras loop in
the CI workflow to add a conditional MCP smoke step when extras is "[mcp]".
After the existing smoke_bare_install.py invocation, import ondine.mcp and call
create_server() using the smoke environment’s Python so the MCP entrypoint and
FastMCP wiring are exercised.

Comment on lines +190 to +197
_assert_answered(
ondine.enrich(
pd.DataFrame(rows), prompt, output_columns=["sentiment"], schema=Sentiment
),
"sentiment",
2,
"enrich(schema=)",
)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assert that schema= used a structured-output request.

This assertion passes if schema=Sentiment is ignored and enrich() uses the plain completion path. Both paths return "positive".

Record whether the stub received response_format.type == "json_schema" or tools. Fail the schema scenario when neither is present. This makes the smoke test detect a loss of structured-output propagation.

Proposed fix
 class _ChatCompletionsStub(BaseHTTPRequestHandler):
+    saw_structured_request = False
+
     def do_POST(self) -> None:
         body = json.loads(self.rfile.read(int(self.headers["Content-Length"])) or "{}")
         response_format = body.get("response_format") or {}
+        if body.get("tools") or response_format.get("type") == "json_schema":
+            type(self).saw_structured_request = True

     class Sentiment(BaseModel):
         sentiment: str

+    _ChatCompletionsStub.saw_structured_request = False
     _assert_answered(
         ondine.enrich(
             pd.DataFrame(rows), prompt, output_columns=["sentiment"], schema=Sentiment
         ),
         "sentiment",
         2,
         "enrich(schema=)",
     )
+    if not _ChatCompletionsStub.saw_structured_request:
+        raise AssertionError("enrich(schema=) did not request structured output")
🤖 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 `@scripts/smoke_bare_install.py` around lines 190 - 197, Update the stub
instrumentation used by the schema scenario around _assert_answered to record
whether the request includes response_format.type equal to "json_schema" or a
tools payload, then assert that structured-output evidence before or alongside
validating the sentiment result. Ensure the enrich(schema=Sentiment) case fails
when neither request field is observed.

@ptimizeroracle
ptimizeroracle merged commit 5f5de06 into main Aug 3, 2026
38 checks passed
@ptimizeroracle
ptimizeroracle deleted the fix/189-bare-install-smoke branch August 3, 2026 16:53
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.

CI: add a bare-install smoke job (pip install ondine, no extras)

1 participant