diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 681952e7..62de906f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,7 +269,7 @@ jobs: # The config's own webServer builds the engine, the design system and the # bundle, copies it into the package data, creates a throwaway workspace and - # starts `visionset ui`. One command, so a failure in any of those steps is + # starts `visionset server`. One command, so a failure in any of those steps is # reported at this step rather than inside a server log. - name: Drive the whole cycle run: pnpm --filter @visionset/app cycle diff --git a/CHANGELOG.md b/CHANGELOG.md index 4004be13..b105e93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,18 @@ nothing was being distributed. This is the first version that is. ### Changed -- **The browser application moved from `/ui` to `/app`.** `visionset ui` now serves it at +- **`visionset ui` is now `visionset server`** (#329). The command starts the FastAPI server; the + browser application is one client of it, alongside REST, the SDK and MCP, so the old name + described a single consumer of the process rather than the process. Nothing else moved — same + flags and defaults, same `/app` mount, same one-sentence refusal at exit 1 outside a workspace. + + **There is no alias.** `visionset ui` now fails with Typer's ordinary `No such command 'ui'`, + so a script that calls the old name needs one edit. A deprecation shim was declined + deliberately rather than overlooked: this is a pre-1.0 beta with no installed base to carry, + and a hidden second spelling of the front door is a thing every later reader has to explain + away. + +- **The browser application moved from `/ui` to `/app`.** `visionset server` now serves it at `http://127.0.0.1:8000/app/`, and `/` still redirects there. One constant moved — `UI_PREFIX` in `src/visionset/server/main.py` — and `frontend/app/vite.config.ts`'s build `base` follows it, which is what keeps the router's basename in agreement: it is read from diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e15b771a..1d88ae7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ a deliberate manual run, because each costs minutes or needs its own install. | Frontend lint | `pnpm -r lint` — **after** a build: `frontend/app` resolves `@visionset/annotator` through its `dist/`, so its typecheck has no declarations until the engine is built | `frontend` | | Annotator headless boundary | `pnpm --filter @visionset/annotator lint` | part of `frontend` (`pnpm -r lint`) | | Annotator end-to-end (chromium) | `pnpm --filter @visionset/app e2e` (needs `playwright install chromium` once) | `browser` | -| Browser cycle (chromium) | `pnpm --filter @visionset/app cycle` — the whole product against a real `visionset ui`; needs `uv sync` and `playwright install chromium`. Repeatable in one workspace: `--repeat-each=N` costs one build rather than N | `browser` | +| Browser cycle (chromium) | `pnpm --filter @visionset/app cycle` — the whole product against a real `visionset server`; needs `uv sync` and `playwright install chromium`. Repeatable in one workspace: `--repeat-each=N` costs one build rather than N | `browser` | | Annotator benchmark (manual) | `pnpm --filter @visionset/app bench` — frame times, recorded not gated | — manual | | Browser client | part of `pnpm test` — `ui-core`'s `data/` suite drives the 401 flow, the token form and the error envelope with a stubbed `fetch`, no server | part of `frontend` | | Design tokens | part of `pnpm test` — `tests/scripts/design_tokens.test.mjs` refuses a colour inside a class name, and `ui-core`'s `tokens.test.ts` gates the stylesheet against its TypeScript mirror | part of `frontend` | @@ -149,7 +149,7 @@ VISIONSET_REQUIRE_WHEEL=1 uv run pytest tests/packaging **The order in that script is the whole point.** `uv build` copies `src/visionset/_static/` as package data *at the moment it runs*, and a fresh checkout's `_static/` holds only `README.md` and `.gitkeep` — so a wheel built before -`pnpm bundle:static` contains **no app at all**. It installs, `visionset ui` starts, and +`pnpm bundle:static` contains **no app at all**. It installs, `visionset server` starts, and `/app/` answers a 404 naming a script the user does not have. There is no error and no traceback anywhere in that sequence, which is why the script checks after each step and why `tests/dist/` checks the artifact rather than the source tree. diff --git a/README.md b/README.md index 90561c3f..bf4185f2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ uv tool install "git+https://github.com/Robomous/VisionSet" # PyPI lands with visionset init ~/datasets/road-signs # a workspace, here and nowhere else cd ~/datasets/road-signs -visionset ui # API at http://127.0.0.1:8000, app at /app +visionset server # API at http://127.0.0.1:8000, app at /app ``` Then follow [the tutorial](docs/tutorial.md): a clip of video to a YOLO dataset in about half an @@ -43,7 +43,7 @@ hour. Full prerequisites — Python 3.12, and ffmpeg only if you are starting fr [docs/install.md](docs/install.md). `init` is the only command that creates a workspace, and it refuses a directory that already holds -something. `visionset ui` run outside one refuses with one sentence and exit 1; it never creates +something. `visionset server` run outside one refuses with one sentence and exit 1; it never creates one, because a command that silently made a workspace out of whatever directory you were standing in is how data ends up somewhere nobody chose. @@ -93,9 +93,9 @@ into 50 deduplicated assets in an approved batch, then shows a re-run creating n ffmpeg. The same cycle runs over each of the other two surfaces, and both start the shipped command for -real: [`examples/http_end_to_end.py`](examples/http_end_to_end.py) starts `visionset ui` on a free -port and drives the API with `urllib` and a bearer token — multipart upload, 202-and-poll ingest, -hash-checked manifest and a 401 it asserts — while +real: [`examples/http_end_to_end.py`](examples/http_end_to_end.py) starts `visionset server` on a +free port and drives the API with `urllib` and a bearer token — multipart upload, 202-and-poll +ingest, hash-checked manifest and a 401 it asserts — while [`examples/mcp_end_to_end.py`](examples/mcp_end_to_end.py) spawns `visionset mcp` and talks JSON-RPC down its pipe, scaling every box out of the preview it saw and into the asset's own pixels. @@ -136,7 +136,7 @@ uv sync # Python env + dev tools pnpm install # frontend workspace ``` -Then `uv run visionset ui` and `pnpm --filter @visionset/app dev`. Or run the whole thing in +Then `uv run visionset server` and `pnpm --filter @visionset/app dev`. Or run the whole thing in containers instead, with nothing installed on the host and nothing built. ### Run it with Docker, and sign in with nothing diff --git a/docker/api-dev.sh b/docker/api-dev.sh index ed71e8b8..ca1097bb 100755 --- a/docker/api-dev.sh +++ b/docker/api-dev.sh @@ -46,7 +46,7 @@ fi # `--reload-dir` is not tidiness. uvicorn's default watch list is the working # directory, which here is the whole bind-mounted repository: `node_modules/` and # `workspace-data/` among the rest — so every SQLite write during an ingest would -# restart the server mid-run. `visionset ui` scopes its own `reload_dirs` to the +# restart the server mid-run. `visionset server` scopes its own `reload_dirs` to the # package for exactly this reason; raw uvicorn does not inherit that, so the scope # is stated here instead. exec uvicorn visionset.server.main:app \ diff --git a/docker/compose.yaml b/docker/compose.yaml index 52be5b6e..1088e295 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -114,7 +114,7 @@ services: # port open to every interface would hand the workspace to the LAN. The two # lines belong together and neither is safe alone. VISIONSET_UI_SESSION: always - # Raw uvicorn rather than `visionset ui`, and deliberately so: that + # Raw uvicorn rather than `visionset server`, and deliberately so: that # command also serves the compiled bundle at /app, which in development is # vite's job on :5173 — and building that bundle is exactly what this stack # exists to avoid needing. diff --git a/docker/nginx.conf b/docker/nginx.conf index d421a68b..e339d7c3 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,7 +1,7 @@ # The dev stack's one entry point. Everything the browser asks for arrives here. # # Dev only. In production there is no proxy and no second origin: the wheel's -# `visionset ui` serves the API at the root and the compiled bundle at /app, from +# `visionset server` serves the API at the root and the compiled bundle at /app, from # one uvicorn process. Nothing in this file describes how VisionSet is deployed. events {} diff --git a/docs/README.md b/docs/README.md index 53ff274a..b30c16c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,4 +36,4 @@ rather than here — it is the file to read *before* building any screen, and th | [mcp-walkthrough.md](mcp-walkthrough.md) | A session over MCP, start to finish: the cycle in the order an agent meets it, and then what twelve real agent runs did with it — where the coordinate frame held, how refusals were read, and the two pieces of friction that changed the tools | | [ui.md](ui.md) | The browser client: why no screen calls `fetch`, how a refusal is read (branch on `code`, and the two codes the client adds), where the token is kept and the three alternatives that were rejected, why a 401 is handled in one subscription, the loading/empty/error component, polling, and the dev proxy that keeps CORS out of production | | [releasing.md](releasing.md) | Cutting a release: what ships, why the beta goes to PyPI as a pre-release, the npm scope, the order the steps have to happen in, and the one step that needs credentials this repository does not hold | -| [cli.md](cli.md) | The command line: the whole cycle as a script, the three exit codes (and why one of them also means "no"), why stdout is data and stderr is prose, what `--json` promises and how it stays the API's shape, why `--workspace` follows the subcommand, and what `visionset init` and `visionset ui` each do | +| [cli.md](cli.md) | The command line: the whole cycle as a script, the three exit codes (and why one of them also means "no"), why stdout is data and stderr is prose, what `--json` promises and how it stays the API's shape, why `--workspace` follows the subcommand, and what `visionset init` and `visionset server` each do | diff --git a/docs/api.md b/docs/api.md index f48129f9..7e1fe915 100644 --- a/docs/api.md +++ b/docs/api.md @@ -7,11 +7,10 @@ endpoint — what a failure looks like, and how to read one. The routes themselves are described by [`openapi.json`](../openapi.json) at the repo root, which is generated (`uv run python scripts/export_openapi.py`) and diffed in CI. Never hand-edit it. -For a worked external client, [`examples/http_end_to_end.py`](../examples/http_end_to_end.py) -starts `visionset ui` on a free port and drives the whole cycle — upload, launch-and-poll ingest, -annotate, promote, publish, verify, export — with `urllib` and a bearer token, and nothing else. -It is deliberately dependency-free: a contract only a smart client can drive is not really a -contract. +For a worked external client, [`examples/http_end_to_end.py`](../examples/http_end_to_end.py) starts +`visionset server` on a free port and drives the whole cycle — upload, launch-and-poll ingest, +annotate, promote, publish, verify, export — with `urllib` and a bearer token, and nothing else. It +is deliberately dependency-free: a contract only a smart client can drive is not really a contract. ## Authentication @@ -293,8 +292,8 @@ because somebody added it to an entity. ## Where the UI lives -The compiled application is mounted at **`/app`** and `/` redirects to it. `visionset ui` starts -both halves with one command; see [cli.md](cli.md#visionset-ui). +The compiled application is mounted at **`/app`** and `/` redirects to it. `visionset server` starts +both halves with one command; see [cli.md](cli.md#visionset-server). **The API owns the root, and that is why the app does not.** `/projects/{project_id}` is a shipped route, so an application served from `/` could never claim `/projects/abc` as one of its *own* diff --git a/docs/auth.md b/docs/auth.md index 5c06c439..61fbb4f6 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -116,7 +116,8 @@ browser -> GET /session Set-Cookie: visionset_session=…; HttpOnly; SameSite=Strict; Path=/ ``` -`visionset ui`, then a browser, and you are on the project list. Nothing typed and nothing copied. +`visionset server`, then a browser, and you are on the project list. Nothing typed and nothing +copied. **The token is untouched.** `Authorization: Bearer` still authenticates every route, the SDK, the CLI, MCP and every third-party client are unaffected, and `AuthProvider.verify(token) -> bool` did @@ -224,10 +225,10 @@ branches it can only reach two: **`VISIONSET_WORKSPACE`**, then the nearest work the working directory. The precedence table and the argument for why only that last case walks upward live in [workspaces.md](workspaces.md#which-workspace-when-nobody-said). -A server started by **`visionset ui`** always lands on the first of those. That command applies the -full precedence itself — including `--workspace`, which no server can see — and then exports the +A server started by **`visionset server`** always lands on the first of those. That command applies +the full precedence itself — including `--workspace`, which no server can see — and then exports the answer, so the two resolvers cannot disagree about a workspace one of them was told about and the -other was not. See [cli.md](cli.md#visionset-ui). +other was not. See [cli.md](cli.md#visionset-server). It is opened by the first request that needs it and kept for the life of the process — never at import time, because `scripts/export_openapi.py` imports the application in a checkout that has no diff --git a/docs/cli.md b/docs/cli.md index 81741825..f4ee33e2 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -37,7 +37,7 @@ visionset backfill-thumbnails --project P visionset token create --name NAME visionset token list visionset token revoke NAME [--yes] -visionset ui [--host] [--port] [--reload] # no --json +visionset server [--host] [--port] [--reload] # no --json visionset mcp # stdio; no --json ``` @@ -84,7 +84,7 @@ actually do. It does not rewrite the kernel's sentence: bending a domain message is how the other surfaces end up with the wrong wording. ``` -$ visionset ui +$ visionset server Error: /tmp is not a VisionSet workspace (no visionset.db); use WorkspaceService.init to create one Point at one with --workspace, or set VISIONSET_WORKSPACE. $ echo $? @@ -207,7 +207,7 @@ Creates a workspace, which every other command needs and none of them makes. $ visionset init ./datasets/robots Created workspace 'robots' at /home/you/datasets/robots. /home/you/datasets/robots -Next: visionset token create --name , then visionset ui. +Next: visionset token create --name , then visionset server. ``` The root is the only thing on stdout, so `WS=$(visionset init ./robots)` is exactly the path — and @@ -221,18 +221,18 @@ it is the *resolved* path, which is the useful answer when you typed `.`. Creating one where a workspace already sits is refused too — the remedy is to use it, not to make a second. Both refusals are one sentence at exit 1. -Deliberately **not** folded into `visionset ui`, which would have to create a workspace when the +Deliberately **not** folded into `visionset server`, which would have to create a workspace when the directory looked empty: that breaks "`init` creates, `open` never does" and means a mistyped path silently becomes a new empty workspace instead of an error. See [workspaces.md](workspaces.md#at-a-terminal). -## `visionset ui` +## `visionset server` Starts the server against the resolved workspace, serving the REST API at the root and the compiled UI bundle at `/app`; `/` redirects to the app. ``` -$ visionset ui +$ visionset server VisionSet 0.0.1.dev0 workspace /home/you/datasets/robots UI and API http://127.0.0.1:8000/ @@ -309,7 +309,7 @@ content addressing means it also creates no asset it created before, which is th interrupted run. The batch id goes to stdout. `--fps` is video-only and a usage error on a folder. The run is **synchronous**, and there is no -`--resume`: polling needs a second process, which is what `visionset ui` and +`--resume`: polling needs a second process, which is what `visionset server` and `GET /ingest-jobs/{id}` are for. See [ingest.md](ingest.md#at-a-terminal). ### `visionset batch` @@ -393,12 +393,12 @@ looks like. visionset mcp [--workspace PATH] ``` -Normally a client spawns it rather than a person running it. Like `ui`, it resolves the workspace +Normally a client spawns it rather than a person running it. Like `server`, it resolves the workspace with the full precedence and then **states** the answer in `VISIONSET_WORKSPACE`, so the server it starts cannot disagree with it, and it opens the workspace first so that `NotAWorkspace` is one sentence at exit 1 rather than a refusal inside the agent's first tool call. -The target is named as a module for a subprocess rather than imported, for the reason `ui` names +The target is named as a module for a subprocess rather than imported, for the reason `server` names uvicorn's app by import string — import-linter forbids `visionset.cli` importing `visionset.mcp`. The subprocess inherits stdin and stdout, because those two streams *are* the transport, which is also why this is the one command that prints **nothing at all** on stdout: a stray line would @@ -445,7 +445,7 @@ repository; each module declares its own fixtures, including an autouse one that `VISIONSET_WORKSPACE` so a developer with it exported gets CI's results. **Use `monkeypatch.setenv(VAR, "")` if any command the module exercises can write `os.environ`, and `delenv(VAR, raising=False)` otherwise** — `delenv` records no undo when the variable was already -absent, so a written one leaks into every later module. Only `ui` writes it today. +absent, so a written one leaks into every later module. Only `server` writes it today. **A test module's basename must be unique across the whole suite.** With no `__init__.py` anywhere, pytest imports a test module under its bare basename, so `tests/cli/test_batches.py` beside diff --git a/docs/examples.md b/docs/examples.md index 11d2c6d7..d6d3aa16 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -217,7 +217,7 @@ real dependency since #16, so a second hand-rolled PNG encoder beside it would b | Stage | What happens | | --- | --- | | Setup | `WorkspaceService.init` then `TokenService.create` — the last SDK lines in the file | -| Serve | `visionset ui --host 127.0.0.1 --port --workspace ` as a subprocess, polled at `/health` until it answers | +| Serve | `visionset server --host 127.0.0.1 --port --workspace ` as a subprocess, polled at `/health` until it answers | | Project | `POST /projects`, `POST /projects/{p}/schema/versions` | | Upload | `POST /projects/{p}/sources/images` — four PNGs as `multipart/form-data`, built by hand | | Ingest | `POST /sources/{s}/ingest-jobs` → **202** + `Location`, then `GET /ingest-jobs/{id}` until it settles | @@ -237,7 +237,7 @@ nothing else — not `httpx`, not `requests`, not `curl`. That is two arguments contract only a smart client can drive is not really a contract. The multipart body is twenty-odd lines in the file, written out rather than delegated, and it is the price of the claim. -**The server actually starts.** [`tests/cli/test_ui.py`](../tests/cli/test_ui.py) patches +**The server actually starts.** [`tests/cli/test_server.py`](../tests/cli/test_server.py) patches `uvicorn.run` and asserts the arguments, which is right for a unit test and says nothing about whether the process comes up. This example binds an ephemeral port, spawns the shipped command against it, and waits for `/health` — the one unauthenticated route, and therefore the readiness @@ -342,7 +342,7 @@ travels the way it really travels: resolved by the CLI, opened once to run any m **One session for the whole walk.** `tests/mcp/_flow.py` opens a fresh session per call, which is convenient for a test and is not what a client does. Holding one open is safe because the server opens and closes the workspace *inside each tool call* — so a long-lived session holds no SQLite -handle and locks nobody out of `visionset ui`. +handle and locks nobody out of `visionset server`. **The pixels an agent sees are not the frame its coordinates live in.** The frames are 640×480 on purpose. The preview is capped at 256 pixels on its long edge, so what arrives is 256×192 and diff --git a/docs/ingest.md b/docs/ingest.md index 932cc06d..a95872db 100644 --- a/docs/ingest.md +++ b/docs/ingest.md @@ -311,7 +311,7 @@ it would print a traceback rather than a sentence. A missing path is exit 2 for **The run is synchronous, and the CLI never calls `enqueue`.** A queued job needs a worker to pick it up, and a CLI process has none — a detached job would simply never run. Polling is what the -server is for: `visionset ui`, then `GET /ingest-jobs/{id}`. +server is for: `visionset server`, then `GET /ingest-jobs/{id}`. **Interrupting a run leaves the job row at `running`, and there is no `--resume`.** The remedy needs no new vocabulary: run the same line again. Registration finds the same source, `enqueue` does not diff --git a/docs/mcp.md b/docs/mcp.md index 8433dbf0..bfc5a4fd 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -246,8 +246,8 @@ beside the workspace and has the filesystem. **One workspace per server.** No tool takes a workspace parameter — threading one through thirty-odd tools would put a path an agent has no way to know into every call. The workspace is -opened and closed per tool call rather than held, so the file is never kept from `visionset ui` or -a second agent between calls. +opened and closed per tool call rather than held, so the file is never kept from `visionset server` +or a second agent between calls. **A discriminated union's `type` must be spelled out.** `geometry` and the partition variants carry a default on their tag, so the generated schema shows `type` as optional — but it is read diff --git a/docs/releasing.md b/docs/releasing.md index f3f14b36..c09e5f6b 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -139,4 +139,4 @@ visionset format list # coco, dummy, voc, yolo `format list` is the useful one: it reads installed entry-point metadata, so a non-empty answer proves the distribution was assembled correctly and not merely uploaded. Then confirm the other -half of the thesis — `visionset init` somewhere, `visionset ui`, and open `/app`. +half of the thesis — `visionset init` somewhere, `visionset server`, and open `/app`. diff --git a/docs/tutorial.md b/docs/tutorial.md index d7bea6fc..db7652b3 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -106,7 +106,7 @@ decision rather than a removal, so a batch always describes what was actually lo Now open the app: ```bash -visionset ui +visionset server ``` The API is at `http://127.0.0.1:8000` and the browser app at `http://127.0.0.1:8000/app`. diff --git a/docs/ui.md b/docs/ui.md index cdb9b7a9..f15b2792 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -20,7 +20,7 @@ panel and the annotation page. `@visionset/app` is a router, a rail and nothing | Playwright (browser cycle) | — | **1**, against a real server | | Python | 1923 | **1932** | -The exit criterion — *"with `visionset ui` running, a user completes the entire cycle +The exit criterion — *"with `visionset server` running, a user completes the entire cycle in the browser"* — is not asserted, it is **driven**: `pnpm --filter @visionset/app cycle` walks token → project → schema → ingest → approve → annotate → finish → complete → promote → publish → verify → export → download against the built bundle @@ -668,7 +668,7 @@ stops using it here, and a reload signs you back in. Refusals are told apart by what to do next, not by status: a 401 says the token was refused (mistyped, revoked, or minted for a different workspace — the API answers one identical 401 for all four cases and a client must not pretend otherwise), and a -`NETWORK_ERROR` says the server is not answering and names `visionset ui`. +`NETWORK_ERROR` says the server is not answering and names `visionset server`. ### Where it is kept, and why @@ -735,7 +735,7 @@ polling a state somebody adds later. `ApiProvider` takes `baseUrl` and the app decides it — a library that reads `import.meta.env` is a library that can only be built one way. -- **Production**: `""`. `visionset ui` serves the API at the root and the bundle at +- **Production**: `""`. `visionset server` serves the API at the root and the bundle at `/app`, so a relative request already lands on it. - **Development**: `"/api"`, proxied by vite to `http://127.0.0.1:8000` (override with `VISIONSET_API`). @@ -780,7 +780,7 @@ The rules: ## The browser cycle `pnpm --filter @visionset/app cycle` runs the whole product against a real server: -`visionset ui` serving the built bundle out of `_static/`, the real API, the real +`visionset server` serving the built bundle out of `_static/`, the real API, the real kernel, and no mocks anywhere. Token → project → schema → ingest → approve → annotate → finish → complete → promote → publish → verify → export → download. diff --git a/docs/workspaces.md b/docs/workspaces.md index dd8e75bf..fa872f6b 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -181,8 +181,9 @@ Precedence, first match wins: | 3 | the nearest directory at or above the working directory holding a `visionset.db` | **yes** | | 4 | the working directory | — | -A server started by import string has no argv, so it reaches only 2, 3 and 4. When `visionset ui` -started it, it reaches only 2: that command resolves through all four branches, opens the result, +A server started by import string has no argv, so it reaches only 2, 3 and 4. When +`visionset server` started it, it reaches only 2: that command resolves through all four branches, +opens the result, and then *states* it in `VISIONSET_WORKSPACE` — one decision, made once, at the surface a person is standing at. It has to travel that way rather than as an argument, because `create_app()` takes no parameters and `--reload` runs the application in a separate process. @@ -197,8 +198,8 @@ client that spawns `visionset` directly sets `VISIONSET_WORKSPACE` in the server handle in `create_app()` and keeps it for the process's life, because it is one long-lived reader of many requests. The CLI and the MCP server open and close per command and per *tool call*: there is then no module-level state to tear down between tests, and — since SQLite has one writer — a -stdio server that held the file between calls would keep `visionset ui` and a second agent out of -a workspace nobody is using. `close()` checkpoints the WAL, so neither leaves a `visionset.db-wal` +stdio server that held the file between calls would keep `visionset server` and a second agent out +of a workspace nobody is using. `close()` checkpoints the WAL, so neither leaves a `visionset.db-wal` behind. **Only case 3 walks, and that asymmetry is the whole rule.** A flag and an environment variable are diff --git a/examples/README.md b/examples/README.md index ab6e3ac2..628e4447 100644 --- a/examples/README.md +++ b/examples/README.md @@ -83,7 +83,7 @@ uv run python examples/http_end_to_end.py ./scratch # or wherever you like ``` Same destination rules as above. It creates a workspace, mints one token, then starts -`visionset ui` on an unused loopback port and does everything else through `urllib` — four +`visionset server` on an unused loopback port and does everything else through `urllib` — four images uploaded as multipart, an ingest launched with 202 and polled to completion, two jobs annotated, a release published and verified, an archive downloaded, and one request sent without the token to prove it is refused. @@ -93,7 +93,7 @@ the release manifest, whose bytes hash to the release's own `manifest_hash`, and archive. Serve it again yourself and keep going: ```bash -visionset ui --workspace examples/workspace-data/http-e2e/ws +visionset server --workspace examples/workspace-data/http-e2e/ws curl -s -H "Authorization: Bearer $(visionset token create --name scratch \ --workspace examples/workspace-data/http-e2e/ws)" localhost:8000/projects ``` diff --git a/examples/http_end_to_end.py b/examples/http_end_to_end.py index 2580007c..ed3b0b90 100644 --- a/examples/http_end_to_end.py +++ b/examples/http_end_to_end.py @@ -292,7 +292,7 @@ def main(dest: Path) -> Summary: base_url = f"http://{HOST}:{port}" server = subprocess.Popen( # noqa: S603 [ - "visionset", "ui", + "visionset", "server", "--host", HOST, "--port", str(port), "--workspace", str(root), @@ -300,7 +300,7 @@ def main(dest: Path) -> Summary: ) # fmt: skip try: wait_until_serving(server, base_url) - _say(f"`visionset ui` is serving {base_url}") + _say(f"`visionset server` is serving {base_url}") return _walk(Client(base_url, token), base_url, downloads) finally: server.terminate() @@ -575,7 +575,7 @@ def _run() -> None: f"\nDone. {summary.promoted} assets and {summary.annotation_count} labels released as " f"{summary.release_tag}, verified {summary.verified}, over {summary.base_url}.\n" f"Workspace left at {dest / 'ws'} — serve it again with " - f"`visionset ui --workspace {dest / 'ws'}`." + f"`visionset server --workspace {dest / 'ws'}`." ) diff --git a/frontend/app/cycle/cycle.spec.ts b/frontend/app/cycle/cycle.spec.ts index 7d8fca8e..d1ca2535 100644 --- a/frontend/app/cycle/cycle.spec.ts +++ b/frontend/app/cycle/cycle.spec.ts @@ -3,7 +3,7 @@ * * Token → project → schema → ingest → approve → annotate → complete → promote → * publish → export → download. No mocks anywhere: the bundle is the built one that - * ships in the wheel, and `visionset ui` serves it beside the real API over the + * ships in the wheel, and `visionset server` serves it beside the real API over the * real kernel. * * ## One test, and that is deliberate @@ -121,7 +121,7 @@ test("the whole cycle, from opening the app to a downloaded export", async ({ pa await test.step("open the app, which asks for nothing", async () => { await page.goto("./"); - // #179's first acceptance criterion, against the real thing: `visionset ui` + // #179's first acceptance criterion, against the real thing: `visionset server` // on this machine, a browser, and the product — nothing typed, nothing // pasted, no token anywhere in this step. await expect(page.getByTestId("app-rail")).toBeVisible(); diff --git a/frontend/app/playwright.cycle.config.ts b/frontend/app/playwright.cycle.config.ts index ce317176..5b562c81 100644 --- a/frontend/app/playwright.cycle.config.ts +++ b/frontend/app/playwright.cycle.config.ts @@ -3,7 +3,7 @@ * * Every other suite in this repository stubs the API — `annotate.spec.ts` holds * the routes still so a failure names the page, and the annotator's 76 scenarios - * have no server at all. This one has no mocks anywhere: `visionset ui` serves the + * have no server at all. This one has no mocks anywhere: `visionset server` serves the * compiled bundle out of `_static/` and the API off the same origin, exactly as the * wheel does, and Playwright drives the product from a pasted token to a downloaded * export. @@ -72,7 +72,7 @@ export default defineConfig({ }, webServer: { // The whole stack, in the order the wheel builds it: engine, design system, - // bundle, and the bundle copied into the package data `visionset ui` serves. + // bundle, and the bundle copied into the package data `visionset server` serves. command: [ "pnpm --filter @visionset/annotator build", "pnpm --filter @visionset/ui-core build", diff --git a/frontend/app/src/main.tsx b/frontend/app/src/main.tsx index 7e469849..2b459d22 100644 --- a/frontend/app/src/main.tsx +++ b/frontend/app/src/main.tsx @@ -18,7 +18,7 @@ * * ## `BrowserRouter`, and the basename the wheel needs * - * `visionset ui` serves the bundle under `/app` — the API owns the root, which + * `visionset server` serves the bundle under `/app` — the API owns the root, which * `UI_PREFIX`'s docstring argues is a consequence of an unprefixed API rather than * something a later milestone can lift. So the router's basename has to match * vite's `base`, and both are read from the same place: `import.meta.env.BASE_URL` diff --git a/frontend/app/vite.config.ts b/frontend/app/vite.config.ts index 66b3c930..e0c95e3d 100644 --- a/frontend/app/vite.config.ts +++ b/frontend/app/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig(({ command }) => ({ server: { // The dev proxy, and the reason the server has no CORS middleware. // - // In production there is no cross-origin problem to solve: `visionset ui` + // In production there is no cross-origin problem to solve: `visionset server` // serves the API at the root and the bundle at `/app`, so the app asks for // `/projects` on its own origin. In development vite owns the origin and the // API is somewhere else, and the two ways to bridge that are not equal — diff --git a/frontend/ui-core/src/data/ApiProvider.tsx b/frontend/ui-core/src/data/ApiProvider.tsx index e446a88b..7c7fa147 100644 --- a/frontend/ui-core/src/data/ApiProvider.tsx +++ b/frontend/ui-core/src/data/ApiProvider.tsx @@ -117,7 +117,7 @@ export interface ApiProviderProps { /** * Where the API lives, **without** a trailing slash. * - * `""` — same origin — is what production uses: `visionset ui` serves the bundle + * `""` — same origin — is what production uses: `visionset server` serves the bundle * at `/app` and the API at the root, so a relative request already lands on it. * Development points at a proxy prefix instead; the app decides, because a * library that reads `import.meta.env` is a library that can only be built one diff --git a/frontend/ui-core/src/data/TokenGate.tsx b/frontend/ui-core/src/data/TokenGate.tsx index a646a436..cd302e00 100644 --- a/frontend/ui-core/src/data/TokenGate.tsx +++ b/frontend/ui-core/src/data/TokenGate.tsx @@ -5,7 +5,7 @@ * whole job is to notice which one applies before showing anybody a form. * * **A browser session.** The server signs in the page it served itself, over an - * `HttpOnly` cookie (#179). Opening `visionset ui` on the machine it runs on + * `HttpOnly` cookie (#179). Opening `visionset server` on the machine it runs on * reaches the product with nothing typed and nothing copied, because asking * somebody to paste a credential to read their own files off their own disk is * ceremony with no threat model behind it. This component is what asks — once, on @@ -41,7 +41,7 @@ * sentence, and a pointer at the command that mints one. The *other* failures are * worth telling apart, and are: a server that is not running (`NETWORK_ERROR`) is * the most likely failure of all on a local-first tool, and saying "check the - * token" to somebody who has not started `visionset ui` sends them the wrong way + * token" to somebody who has not started `visionset server` sends them the wrong way * for ten minutes. */ @@ -179,7 +179,7 @@ function refusalOf(cause: unknown): string { return "That token was refused. It may be mistyped, revoked, or minted for a different workspace."; } if (failure.code === NETWORK_ERROR) { - return "No answer from the server. Is `visionset ui` running?"; + return "No answer from the server. Is `visionset server` running?"; } // Everything else through the shared vocabulary. The two branches above stay // local because they are about *this* screen — a token being refused and a diff --git a/frontend/ui-core/src/data/dataShell.test.tsx b/frontend/ui-core/src/data/dataShell.test.tsx index a79d4a6c..424d6985 100644 --- a/frontend/ui-core/src/data/dataShell.test.tsx +++ b/frontend/ui-core/src/data/dataShell.test.tsx @@ -13,7 +13,7 @@ * * ## Why the base URL is absolute here and empty in production * - * Production passes `""` — same origin — because `visionset ui` serves the bundle + * Production passes `""` — same origin — because `visionset server` serves the bundle * at `/app` and the API at the root, so a relative request already lands on it. That * cannot be exercised under vitest: jsdom does **not** replace Node's `Request`, and * undici's requires an absolute URL, so `new Request("/projects")` throws *"Failed @@ -285,7 +285,7 @@ describe("the token form", () => { await userEvent.click(screen.getByTestId("token-submit")); await waitFor(() => expect(screen.queryByTestId("token-error")).not.toBeNull()); - expect(screen.getByTestId("token-error").textContent).toContain("visionset ui"); + expect(screen.getByTestId("token-error").textContent).toContain("visionset server"); vi.unstubAllGlobals(); }); diff --git a/scripts/cycle_server.sh b/scripts/cycle_server.sh index cc6df2b6..758c2e6b 100755 --- a/scripts/cycle_server.sh +++ b/scripts/cycle_server.sh @@ -5,7 +5,7 @@ # steps can fail in ways worth naming, and a `&&` chain reports only the exit code. # # It creates a throwaway workspace, mints a token into a file the suite reads, and -# then **execs** the server: `visionset ui` inherits the process, so Playwright's +# then **execs** the server: `visionset server` inherits the process, so Playwright's # own shutdown reaches uvicorn rather than a shell that outlives it. set -euo pipefail @@ -44,4 +44,4 @@ for index, colour in enumerate([(210, 90, 70), (70, 140, 210), (120, 190, 120)]) PY echo "cycle workspace ready at $ws" >&2 -exec visionset ui --workspace "$ws" --host 127.0.0.1 --port "$port" +exec visionset server --workspace "$ws" --host 127.0.0.1 --port "$port" diff --git a/src/visionset/_static/README.md b/src/visionset/_static/README.md index afd1ac82..9e1aaa1a 100644 --- a/src/visionset/_static/README.md +++ b/src/visionset/_static/README.md @@ -2,7 +2,7 @@ At build time, the compiled `@visionset/app` bundle (`frontend/app/dist/`) is copied here by the root script `pnpm bundle:static`, so the UI travels **inside the single Python wheel** as -package data and `visionset ui` can serve it with zero extra downloads. +package data and `visionset server` can serve it with zero extra downloads. Everything in this directory except this README and `.gitkeep` is git-ignored: the bundle is a build artifact, never a committed source. This README and `.gitkeep` are also what make the diff --git a/src/visionset/cli/ingest.py b/src/visionset/cli/ingest.py index 6200c083..2f0cfc79 100644 --- a/src/visionset/cli/ingest.py +++ b/src/visionset/cli/ingest.py @@ -17,7 +17,7 @@ new vocabulary. **The run is synchronous, and nothing polls it.** The kernel writes progress to -the job row for a *second process* to read (that is what ``visionset ui`` and +the job row for a *second process* to read (that is what ``visionset server`` and ``GET /ingest-jobs/{id}`` are for); a CLI that queued the work would have no worker to run it. So this blocks, says so on stderr first, and prints the batch id when it is done. diff --git a/src/visionset/cli/init.py b/src/visionset/cli/init.py index 3733ddb0..090afeb6 100644 --- a/src/visionset/cli/init.py +++ b/src/visionset/cli/init.py @@ -59,4 +59,4 @@ def init( workspace.close() note(f"Created workspace {created!r} at {root}.") typer.echo(str(root)) - note("Next: visionset token create --name , then visionset ui.") + note("Next: visionset token create --name , then visionset server.") diff --git a/src/visionset/cli/main.py b/src/visionset/cli/main.py index 0d0f6ae0..ae8a1391 100644 --- a/src/visionset/cli/main.py +++ b/src/visionset/cli/main.py @@ -17,8 +17,8 @@ from visionset.cli.projects import project_app from visionset.cli.releases import release_app from visionset.cli.schemas import schema_app +from visionset.cli.server import server from visionset.cli.tokens import token_app -from visionset.cli.ui import ui app = typer.Typer( name="visionset", @@ -35,9 +35,9 @@ # cycle is shown in one sequence. # # Bare commands are registered here rather than decorated at their definition -# site: a ``@app.command()`` in ``ui.py`` would have to import this module, which -# imports ``ui.py``. Typer reads a command's annotations out of its *defining* -# module's globals either way, which is what lets the shared ``WorkspaceOption`` +# site: a ``@app.command()`` in ``server.py`` would have to import this module, +# which imports ``server.py``. Typer reads a command's annotations out of its +# *defining* module's globals either way, which is what lets the shared ``WorkspaceOption`` # and ``JsonOption`` aliases resolve there. The name is spelled out rather than # derived from the function, so ``backfill-thumbnails`` is not a guess. app.command("init")(init) @@ -51,7 +51,7 @@ app.add_typer(format_app, name="format") app.command("backfill-thumbnails")(backfill_thumbnails) app.add_typer(token_app, name="token") -app.command()(ui) +app.command()(server) app.command()(mcp) diff --git a/src/visionset/cli/mcp.py b/src/visionset/cli/mcp.py index b9918eaf..6588cd16 100644 --- a/src/visionset/cli/mcp.py +++ b/src/visionset/cli/mcp.py @@ -1,12 +1,12 @@ # usage: from visionset.cli.mcp import mcp """``visionset mcp`` — the front door for an agent: one command, the whole listing. -``ui.py``'s shape exactly, with a subprocess where that one has uvicorn, and the +``server.py``'s shape exactly, with a subprocess where that one has uvicorn, and the same three decisions behind it. **The server is named, never imported.** import-linter forbids ``visionset.cli`` importing ``visionset.mcp``, so the target is spelled as a module for the -interpreter to find. ``ui`` gets to hand uvicorn an import string; there is no +interpreter to find. ``server`` gets to hand uvicorn an import string; there is no equivalent here, so this spawns ``python -m visionset.mcp.main`` and lets it inherit stdin and stdout — which is the whole point, because those two streams *are* the MCP transport. Nothing is captured, nothing is piped, and this process @@ -22,7 +22,7 @@ real ``open``: it runs the migration, so ``NotAWorkspace``, ``WorkspaceCorrupt`` and ``WorkspaceFormatTooNew`` land at a terminal as one sentence and exit 1 rather than inside the agent's first tool call, where the answer is a JSON envelope -nobody is watching. Closing again matters for the same reason it does in ``ui``: +nobody is watching. Closing again matters for the same reason it does in ``server``: an uncheckpointed SQLite leaves ``visionset.db-wal`` behind, and the child is about to open the file for itself. diff --git a/src/visionset/cli/ui.py b/src/visionset/cli/server.py similarity index 97% rename from src/visionset/cli/ui.py rename to src/visionset/cli/server.py index 0c164aff..78a90a9b 100644 --- a/src/visionset/cli/ui.py +++ b/src/visionset/cli/server.py @@ -1,5 +1,5 @@ -# usage: from visionset.cli.ui import ui -"""``visionset ui`` — the front door: one command, the API and the app. +# usage: from visionset.cli.server import server +"""``visionset server`` — the front door: one command, the API and the app. **The server is named, never imported.** ``uvicorn.run`` is handed the import string ``visionset.server.main:app`` because import-linter forbids @@ -84,7 +84,7 @@ def _package_dir() -> Path: return Path(str(resources.files("visionset"))) -def ui( +def server( host: Annotated[str, typer.Option("--host", help="Address to bind.")] = DEFAULT_HOST, port: Annotated[int, typer.Option("--port", help="Port to bind.")] = DEFAULT_PORT, reload: Annotated[ diff --git a/src/visionset/mcp/_workspace.py b/src/visionset/mcp/_workspace.py index 734f26ed..470d8f39 100644 --- a/src/visionset/mcp/_workspace.py +++ b/src/visionset/mcp/_workspace.py @@ -23,7 +23,7 @@ would have to be torn down between tests and would leak a workspace into the next module when a test forgot. 2. SQLite has one writer. A stdio server that held the file between calls would - keep ``visionset ui`` and a second agent out of a workspace nobody is using. + keep ``visionset server`` and a second agent out of a workspace nobody is using. 3. ``close()`` checkpoints the WAL, so a client that disappears mid-session leaves no ``visionset.db-wal`` behind. diff --git a/src/visionset/server/main.py b/src/visionset/server/main.py index f798c8f3..d458f44b 100644 --- a/src/visionset/server/main.py +++ b/src/visionset/server/main.py @@ -256,7 +256,7 @@ def create_app() -> FastAPI: and so does ``uvicorn visionset.server.main:app``. It takes **no parameters**, and that is a decision rather than an omission. - ``visionset ui`` starts this server by *import string* — import-linter forbids + ``visionset server`` starts this server by *import string* — import-linter forbids ``visionset.cli`` importing ``visionset.server``, and ``uvicorn --reload`` requires the import-string form anyway — so an argument here would be unreachable from the only production caller. Production configures through diff --git a/tests/cli/test_init.py b/tests/cli/test_init.py index 4f724221..c60021e0 100644 --- a/tests/cli/test_init.py +++ b/tests/cli/test_init.py @@ -39,7 +39,7 @@ def test_the_resolved_root_is_the_only_thing_on_stdout(tmp_path: Path) -> None: result = runner.invoke(app, ["init", str(tmp_path / "ws")]) assert result.stdout.strip() == str((tmp_path / "ws").resolve()) assert "Created workspace" in result.stderr - assert "visionset ui" in result.stderr + assert "visionset server" in result.stderr def test_it_names_the_workspace_after_its_directory(tmp_path: Path) -> None: diff --git a/tests/cli/test_mcp_command.py b/tests/cli/test_mcp_command.py index e54c4ccc..e36e85b0 100644 --- a/tests/cli/test_mcp_command.py +++ b/tests/cli/test_mcp_command.py @@ -113,7 +113,7 @@ def test_the_working_directory_is_walked_upward_when_nobody_said( def test_the_flag_pointed_below_a_workspace_does_not_walk_up_to_it( tmp_path: Path, spawn: Spawn ) -> None: - # This branch's own walk-negative, the sibling of `visionset ui`'s and of the + # This branch's own walk-negative, the sibling of `visionset server`'s and of the # kernel's. A stated directory is somebody saying which workspace, and trading # it for its parent is how an agent is pointed at the wrong one. root = _workspace(tmp_path) diff --git a/tests/cli/test_ui.py b/tests/cli/test_server.py similarity index 87% rename from tests/cli/test_ui.py rename to tests/cli/test_server.py index 372cf634..3c1174bd 100644 --- a/tests/cli/test_ui.py +++ b/tests/cli/test_server.py @@ -1,4 +1,4 @@ -"""`visionset ui`: what uvicorn is told, and what happens before it is told anything. +"""`visionset server`: what uvicorn is told, and what happens before it is told anything. Nothing here starts a server. `uvicorn.run` is replaced by a recorder, which is patching the boundary rather than standing in for it — `uvicorn.run` is a @@ -28,7 +28,7 @@ from typer.testing import CliRunner from visionset.cli.main import app -from visionset.cli.ui import APP_IMPORT_STRING +from visionset.cli.server import APP_IMPORT_STRING from visionset.kernel.services import WORKSPACE_ENV_VAR, WorkspaceService runner = CliRunner() @@ -75,7 +75,7 @@ def test_the_server_is_named_by_import_string_and_never_handed_an_object( does not reach one some other way. It is also what ``--reload`` requires, since a worker process cannot be handed an application object. """ - result = runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + result = runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert result.exit_code == 0, result.output assert started[0]["target"] == APP_IMPORT_STRING == "visionset.server.main:app" assert isinstance(started[0]["target"], str) @@ -84,7 +84,7 @@ def test_the_server_is_named_by_import_string_and_never_handed_an_object( def test_it_binds_loopback_on_port_8000_by_default( workspace_root: Path, started: list[dict[str, object]] ) -> None: - runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert started[0]["host"] == "127.0.0.1" assert started[0]["port"] == 8000 @@ -93,7 +93,7 @@ def test_it_binds_the_host_and_port_it_was_given( workspace_root: Path, started: list[dict[str, object]] ) -> None: runner.invoke( - app, ["ui", "--host", "0.0.0.0", "--port", "9999", "--workspace", str(workspace_root)] + app, ["server", "--host", "0.0.0.0", "--port", "9999", "--workspace", str(workspace_root)] ) assert started[0]["host"] == "0.0.0.0" assert started[0]["port"] == 9999 @@ -103,7 +103,7 @@ def test_reload_watches_the_installed_package_and_not_the_working_directory( workspace_root: Path, started: list[dict[str, object]] ) -> None: """uvicorn's own default is the working directory, which here holds node_modules.""" - runner.invoke(app, ["ui", "--reload", "--workspace", str(workspace_root)]) + runner.invoke(app, ["server", "--reload", "--workspace", str(workspace_root)]) assert started[0]["reload"] is True watched = started[0]["reload_dirs"] assert isinstance(watched, list) @@ -114,7 +114,7 @@ def test_without_reload_no_directories_are_named( workspace_root: Path, started: list[dict[str, object]] ) -> None: """uvicorn warns "configuration will not reload" whenever both are not set together.""" - runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert started[0]["reload"] is False assert started[0]["reload_dirs"] is None @@ -126,7 +126,7 @@ def test_the_resolved_workspace_is_exported_before_the_server_starts( workspace_root: Path, started: list[dict[str, object]] ) -> None: """The only channel there is: ``create_app()`` takes no parameters.""" - runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + runner.invoke(app, ["server", "--workspace", str(workspace_root)]) env = started[0]["env"] assert isinstance(env, dict) assert env[WORKSPACE_ENV_VAR] == str(workspace_root) @@ -141,7 +141,7 @@ def test_the_flag_wins_over_the_environment_variable( WorkspaceService.init(root).close() monkeypatch.setenv(WORKSPACE_ENV_VAR, str(ambient)) - result = runner.invoke(app, ["ui", "--workspace", str(flagged)]) + result = runner.invoke(app, ["server", "--workspace", str(flagged)]) assert result.exit_code == 0, result.output env = started[0]["env"] assert isinstance(env, dict) @@ -160,7 +160,7 @@ def test_a_command_run_below_a_workspace_exports_the_one_above( below.mkdir() monkeypatch.chdir(below) - result = runner.invoke(app, ["ui"]) + result = runner.invoke(app, ["server"]) assert result.exit_code == 0, result.output env = started[0]["env"] assert isinstance(env, dict) @@ -180,7 +180,7 @@ def test_the_flag_pointed_below_a_workspace_does_not_walk_up_to_it( below = workspace_root / "notes" below.mkdir() - result = runner.invoke(app, ["ui", "--workspace", str(below)]) + result = runner.invoke(app, ["server", "--workspace", str(below)]) assert result.exit_code == 1 assert started == [] @@ -194,7 +194,7 @@ def test_outside_any_workspace_it_exits_one_and_never_starts_a_server( """Acceptance criterion: a clear error, not a stack trace, and no half-started server.""" monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["ui"]) + result = runner.invoke(app, ["server"]) assert result.exit_code == 1 assert "not a VisionSet workspace" in result.stderr assert "--workspace" in result.stderr @@ -212,7 +212,7 @@ def test_the_preflight_leaves_no_write_ahead_log_beside_the_workspace( The server is about to open the same file; a checkpoint left behind is state the next reader has to recover before it can answer anything. """ - result = runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + result = runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert result.exit_code == 0, result.output assert not (workspace_root / "visionset.db-wal").exists() assert not (workspace_root / "visionset.db-shm").exists() @@ -224,7 +224,7 @@ def test_the_preflight_leaves_no_write_ahead_log_beside_the_workspace( def test_the_banner_names_the_url_the_workspace_and_how_to_mint_a_token( workspace_root: Path, started: list[dict[str, object]] ) -> None: - result = runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + result = runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert result.exit_code == 0, result.output assert "http://127.0.0.1:8000/" in result.stderr assert str(workspace_root) in result.stderr @@ -235,7 +235,7 @@ def test_nothing_is_printed_on_stdout( workspace_root: Path, started: list[dict[str, object]] ) -> None: """Stdout is a command's data, and this command has none.""" - result = runner.invoke(app, ["ui", "--workspace", str(workspace_root)]) + result = runner.invoke(app, ["server", "--workspace", str(workspace_root)]) assert result.exit_code == 0, result.output assert result.stdout == "" @@ -244,6 +244,6 @@ def test_binding_every_interface_still_prints_a_url_a_browser_can_open( workspace_root: Path, started: list[dict[str, object]] ) -> None: """``http://0.0.0.0:8000`` is what a naive banner prints and what nothing opens.""" - result = runner.invoke(app, ["ui", "--host", "0.0.0.0", "--workspace", str(workspace_root)]) + result = runner.invoke(app, ["server", "--host", "0.0.0.0", "--workspace", str(workspace_root)]) assert "http://127.0.0.1:8000/" in result.stderr assert "http://0.0.0.0" not in result.stderr diff --git a/tests/examples/test_http_end_to_end.py b/tests/examples/test_http_end_to_end.py index 8658353c..0daa245b 100644 --- a/tests/examples/test_http_end_to_end.py +++ b/tests/examples/test_http_end_to_end.py @@ -1,7 +1,7 @@ """The HTTP end-to-end example, run as a smoke test. M3's exit criterion turned into a regression guard for one of its three legs: if -a route stops composing with the ones around it, or if `visionset ui` stops +a route stops composing with the ones around it, or if `visionset server` stops being able to start at all, this fails long before anyone runs the example by hand. The assertions are about *outcomes* — what the release holds, whether the manifest still hashes to itself — rather than about the printed narration, which @@ -56,9 +56,9 @@ def summary(example: ModuleType, tmp_path_factory: pytest.TempPathFactory) -> An def test_the_server_actually_started_and_answered(summary: Any) -> None: - """`visionset ui` bound a socket and served the API through it. + """`visionset server` bound a socket and served the API through it. - Nothing else in the suite proves this: `tests/cli/test_ui.py` patches + Nothing else in the suite proves this: `tests/cli/test_server.py` patches `uvicorn.run` and asserts the arguments, which is right for a unit test and says nothing about whether the process comes up. """ diff --git a/tests/packaging/test_wheel.py b/tests/packaging/test_wheel.py index 312f43f7..f02b134a 100644 --- a/tests/packaging/test_wheel.py +++ b/tests/packaging/test_wheel.py @@ -9,7 +9,7 @@ The first is the one worth naming. `uv build` copies `src/visionset/_static/` as package data *at the moment it runs*, and a fresh checkout's `_static/` holds two placeholder files — so a wheel built before `pnpm bundle:static` contains no app -at all. It installs. `visionset ui` starts. `/app/` answers a 404 naming a script +at all. It installs. `visionset server` starts. `/app/` answers a 404 naming a script the user cannot run, because they do not have the repository. There is no error and no traceback anywhere in that sequence. @@ -314,7 +314,7 @@ def _serving(venv: Path, workspace: Path, port: int) -> _Server: class _Server: - """`visionset ui` for the duration of a `with`, or say why it never came up.""" + """`visionset server` for the duration of a `with`, or say why it never came up.""" def __init__(self, venv: Path, workspace: Path, port: int) -> None: self._venv = venv @@ -338,7 +338,7 @@ def _start(self) -> str: self._process = subprocess.Popen( [ str(binary), - "ui", + "server", "--workspace", str(self._workspace), "--port", diff --git a/tests/server/test_session.py b/tests/server/test_session.py index d1b41647..5a37619c 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -13,7 +13,7 @@ cannot: the secret is a *file in the workspace directory*, and `visionset.server.session` finds that directory with `resolve_workspace_root()` — deliberately, because it must not open the database to answer a public route. So -the tests set `VISIONSET_WORKSPACE`, which is exactly what `visionset ui` and the +the tests set `VISIONSET_WORKSPACE`, which is exactly what `visionset server` and the compose stack do, and the handle is pointed at the same path so the two halves cannot disagree.