lace-app-sdk
Testing & proof lanes
Every app ships its own proof harness. lace-app test runs it locally — the same lanes CI runs before sealing a release.
Why proof lanes exist
A builder-generated app reaches publish through the same gate as a hand-written one. The harness (src/lace_app_sdk/testing.py, wired by lace.builder.publisher) is the enforcement point: if a lane fails, the publisher refuses to seal.
Running it locally closes the loop before you push.
The lanes
| Lane | What it checks | Typical failure |
|---|---|---|
typecheck | pyright / mypy across app/ (if configured) | Missing import or typed schema drift |
build | vite build for ui/ + python -m compileall app/ | Broken UI bundle or syntax error |
route_smoke | Mounts AppRouteProviders and curls /health/ready + app routes | Unmounted route or 500 on GET |
tool | Invokes each @tool handler with a representative payload via AppToolRegistry | Descriptor/handler mismatch or missing tool_modules import |
permission | Matrix of RBAC scopes × resource ACLs over routes and tools | Public route leaking tenant data |
app_data | Collections registered, unique constraints hold, full-text indexes exist, GET /notes survives restart | Listing from an in-memory dict (the harness restarts the sidecar and checks persistence) |
isolation | Builder isolation + import-boundary scan (lace.builder.import_boundary) | App imports lace.apps.* directly |
Running it
lace-app test
# → typecheck (pyright/mypy if configured)
# → build (vite + python -m compileall)
# → route smoke (mount providers, GET /health/ready, GET /apps/<app_id>/api/* 2xx)
# → tool invocation (AppToolRegistry handler, real args, 200 schema)
# → permission lane (RBAC scope × resource ACL matrix)
# → AppData contract (collections exist, constraints hold, search indexes present)
lace-app test --dir ./field_intake --json > proof.json
- Requires
lace-app-sdk[runtime]— base install prints a clear hint and exits non-zero if you forget. - Exit code 0 = all lanes passed. Exit code 1 + JSON report = at least one lane failed (use
--jsonto pipe into CI). - Run from the app root (where
lace_app_manifest.jsonlives) or pass--dir.
Writing fixtures
Put tests under tests/. The harness discovers tests/conftest.py for shared fixtures (a seeded app_state, a test tenant, a fake blob store).
Example patterns live in src/lace_app_sdk/examples/notes_app/ — see tests/test_routes.py for a route smoke that actually hits AppDataService.
CI
lace-app push uploads the repo, CI runs the harness in a throwaway environment, and only a green report seals the image. lace-app deploy (direct path, no CI) still runs the same lanes locally before sealing.
Load-bearing pitfalls the lanes catch
- In-memory list for GET —
GET /notesreturning[]after a sidecar restart → automatic harness FAIL. Fix: useAppDataService.list_records(see data collections). - Missing
tool_modules— tool descriptor exists but sidecar registry is empty at invoke time → 404. Fix: addtool_modules: ["app.tools"]to the manifest (tools). - Direct
lace.*import — breaks the facade invariant and will fail the isolation scan. Fix: import fromlace_app_sdk.*.
Next: publishing & releases or CLI reference.