Skip to content

Add hub scaffold documentation and integration tests - #39

Merged
scosman merged 5 commits into
mainfrom
claude/intelligent-davinci-y14wgp
Oct 8, 2026
Merged

scosman merged 5 commits into
mainfrom
claude/intelligent-davinci-y14wgp

Conversation

@scosman

@scosman scosman commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds comprehensive documentation and integration tests for the seahaven new --hub scaffold, ensuring that published worlds work correctly with OpenEnv's tooling and clients.

Key Changes

  • New integration test suite (tests/test_hub_scaffold.py): Validates that a hub scaffold:

    • Passes openenv validate (with expected validation block warning)
    • Has the correct structure for openenv push
    • Serves the app specified in openenv.yaml
    • Configures the Dockerfile correctly with proper port, healthcheck, and PATH
    • Includes proper Hugging Face Space front matter in README
    • Works with AutoEnv.from_hub(..., skip_install=True)
  • Enhanced scaffold generation (src/seahaven/cli/new.py):

    • Added support for rendering hub-specific README sections via templates
    • Introduced HUB_README_TEMPLATES and HUB_README_PARTS to manage Space card and connection instructions
    • New _readme_parts() function to conditionally include hub documentation
  • New README templates:

    • space_card.md.tmpl: Hugging Face Space front matter with docker SDK configuration
    • connecting.md.tmpl: Instructions for connecting to published worlds using AutoEnv.from_hub
  • Updated scaffold files:

    • openenv.yaml: Added app field pointing to the ASGI app, clarified why there's no validation block
    • Dockerfile: Added PATH configuration for venv, HEALTHCHECK endpoint, and explanatory comments
    • README.md: Now includes Space card front matter and connection instructions when --hub is used
  • Documentation updates (src/seahaven/docs/):

    • Expanded "Publishing to a hub" section with details on Space configuration
    • New "Connecting to a published world" subsection with code examples
    • New "The image" subsection explaining Dockerfile behavior and limitations
    • Updated CLI reference to document scaffold differences from OpenEnv
  • Test utilities (tests/serving.py):

    • Extracted serving_app() helper to serve pre-built FastAPI apps, enabling testing of scaffold-generated apps

Notable Implementation Details

  • Hub README sections are conditionally rendered: non-hub worlds get empty strings for these substitutions
  • The scaffold intentionally omits the validation: block (documented as expected behavior)
  • The app field in openenv.yaml is critical for OpenEnv's container providers to start the server
  • Tests verify the complete integration: from file structure through actual HTTP requests to the served app

https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp

scosman and others added 5 commits September 24, 2026 02:13
…hub(skip_install=True)

A published Seahaven world is consumed with
`AutoEnv.from_hub("<owner>/<space>", skip_install=True)` (or `base_url=` for a
self-hosted server), which returns OpenEnv's `GenericEnvClient` and never
installs the world's package or runs discovery. Plain `from_hub` without
`skip_install` is documented as unsupported.

Scaffold (`seahaven new --hub`):
- openenv.yaml gains `app: <package>.openenv_app:app`, which OpenEnv's Modal
  and Daytona providers read from the image to start the server; its comment
  now says what reads the file, and why there is no `validation:` block.
- Dockerfile puts `/app/.venv/bin` on PATH, so the providers' `python -m
  uvicorn <app>` uses the world's interpreter, and adds a HEALTHCHECK on
  /health. How it installs Seahaven is unchanged.
- README.md starts with Hugging Face Space settings (`sdk: docker`,
  `app_port: 8000`, `base_path: /console`) and gains a section on connecting,
  both from new `templates/hub_readme/` fragments. `base_path` is /console
  because every image serves the console, while /web exists only when push
  enables its web interface; push keeps an existing `base_path`. A world
  without `--hub` gets the README it had.

Docs: serving_and_openenv.md "Publishing to a hub" covers the Space settings,
connecting with from_hub, and the image; reference/cli.md lists the three
deviations from an `openenv init` environment (no `validation:` block, no
server/app.py, no openenv-* name or discoverable client).

Tests: tests/test_hub_scaffold.py (serve extra) renders a hub scaffold and
checks it against upstream's own code: run_validation reports only the
missing `validation:` block, validate_env_structure passes, the `app:` value
(read with parse_openenv_app_field) imports and serves /health and the
world's metadata on a real port, the Dockerfile CMD, HEALTHCHECK and PATH
agree with it, the Space settings' port and base_path are served, and
`AutoEnv.from_hub(..., base_url=url, skip_install=True)` drives reset, list
tools, two tool calls, state and close. tests/serving.py gains serving_app().

Not verifiable until Seahaven is on PyPI: the image booting, and from_hub
against a real Space. uv.lock unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp
Publishing to a hub now has one way in: `seahaven new x`, then `seahaven hub`
in the world's directory.

- `seahaven hub` finds the world's root as the other commands do (nearest
  pyproject.toml), reads `[project] name` and imports nothing. It writes the
  five files `openenv push` requires, puts the Space front matter at the top of
  README.md, and adds the connecting section after `## Using it` (or at the
  end). A missing README.md is created with both sections.
- All or nothing, never overwrite: every target is checked first; an existing
  hub file, existing front matter or an existing connecting section refuses
  with every obstacle named and nothing written. A world without
  src/<package>/openenv_app.py is refused. A failed write rolls back the files
  it created.
- `seahaven new` loses `--hub` and the README placeholders; its next steps
  mention `seahaven hub`. Template rendering moves to cli/scaffold.py, shared by
  both commands. `project_root` and `project_name` are public in seahaven.cli,
  with a remedy for commands that have no `--world`.
- Output is byte-identical to the old `new --hub` scaffold.
- Docs: reference/cli.md gains a `seahaven hub` section (the three OpenEnv
  deviations move with it); serving_and_openenv.md and index.md updated.
- Tests: tests/test_cli_hub.py covers placement, every refusal with a
  nothing-written snapshot, rollback, and the CLI entry point;
  test_hub_scaffold.py keeps every check, now driving `new` then `hub`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp
…pytest

`seahaven hub` writes an `__init__.py` at the world's root because `openenv
push` requires one. When the directory has the package's name (any one-word
world), pytest collected the root as a package and imported that file as the
world's package, so every test of a fresh `seahaven new demo` + `seahaven hub`
scaffold failed with "demo has no 'world'".

- The pytest plugin collects a directory holding an `__init__.py` and
  `src/<its own name>/` as a plain directory (optionalhook, so pytest 7 runs
  are unaffected).
- `find_world` recognises the root `__init__.py` standing in for the package,
  which a `tests/__init__.py` or a root `conftest.py` still causes, and fails
  with a one-line message naming both causes and the fixes.
- testing.md states the exception and the one layout rule.
- Tests: the scaffold's own pytest after `seahaven new demo` and `seahaven hub`,
  pytester cases for the shadowing, the collector chosen per layout and both
  causes of the new message; the scaffold subprocess run is a shared helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp
Brings in the 0.5.0 release prep, the docs sweep, seahaven.http, the
scaffold's serve-with-its-extra fix and dev tooling under tools/, on top of
`seahaven hub` replacing `seahaven new --hub`.

Conflicts:
- src/seahaven/cli/__init__.py: kept main's CliError fix/line/root fields
  and placed-import errors, and this branch's public project_root/
  project_name and root-__init__.py SH501. That SH501 now also carries a
  fix=, as main's no-world SH501 does.
- src/seahaven/cli/new.py: kept main's `import sys` for its stdlib-name
  refusal; dropped the template-walking imports, which moved to scaffold.py.
- src/seahaven/docs/reference/cli.md: took main's "prints what to do next"
  with no unpublished warning, kept the `seahaven hub` section, and dropped
  its "while Seahaven is unpublished, the Dockerfile does not build"
  paragraph, which 0.5.0 on PyPI makes false.
- src/seahaven/docs/serving_and_openenv.md: kept this branch's Publishing
  to a hub section and dropped its "Dockerfile does not build a working
  image today" paragraph, as main dropped the same paragraph.
- tests/conftest.py: kept both assert_scaffold_pytest_passes and main's
  broken_tidy helper.
- tests/serving.py: took main's serving_app(ASGIApp, **config), a superset
  of this branch's FastAPI-only one.
- tests/test_cli_new.py: kept main's python_in/build_as_the_readme_says and
  new tests, with the scaffold pytest runs through
  assert_scaffold_pytest_passes. Main's render(..., hub=True) container test
  moved to test_cli_hub.py, driven through add_hub_files.
- tests/test_pytest_plugin.py: kept both sides' new tests.

Outside the markers:
- tests/test_cli_new.py: the no-hub-option test checks the parsed `new`
  namespace instead of passing the removed flag, so the flag's spelling
  appears nowhere outside specs/.
- tests/test_find_world.py: a unit test for the root-__init__.py SH501 and
  its fix.

uv.lock is main's, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp
…-root

The image `seahaven hub` generates could not build, and could not run as the
user a Hugging Face Space runs it as:

- openenv-base:latest ships uv 0.5.27, which can download no final CPython
  3.14, so `uv sync` stopped with "No interpreter found for Python >=3.14".
  Copy in uv 0.12.18 at an exact version before the sync.
- A Space runs the container as uid 1000. uv installed CPython under /root
  (mode 700), so the venv's interpreter was unreadable, and `uv run` in the
  CMD could neither write a cache nor sync the root-owned venv. Install
  CPython under /opt/uv/python, and start the server with
  `python -m uvicorn`, the way OpenEnv's Modal and Daytona providers do.
- The HEALTHCHECK ran a bare `python`, which fell through to the base
  image's 3.11 and passed on a broken image. It now runs the venv's
  interpreter by path.

Tests pin each fix: an exact uv of at least 0.9.0 is copied before the sync,
the install dir is absolute, outside /root and set before the sync, the CMD
does not start with uv, and the HEALTHCHECK uses <WORKDIR>/.venv/bin/python.
The docs say a Space runs the image as user 1000, which cannot write /app.

Verified by host emulation, not a docker build: uv 0.5.27 fails the sync;
a root build into /app with uv 0.12.24 and the template's env serves as uid
1000 (HOME=/ and unset): /health, /console and /web/ return 200, the
HEALTHCHECK passes, and an AutoEnv drive creates and reads an item. The
emulation used uv 0.12.24; the template pins 0.12.18, the uv a real Hugging
Face build used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWJSvm3Hep8BHqyKWBkKvp
@scosman
scosman merged commit 5a50ad1 into main Oct 8, 2026
3 checks passed
@scosman
scosman deleted the claude/intelligent-davinci-y14wgp branch October 8, 2026 21:01
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