diff --git a/docs/configuration.md b/docs/configuration.md index 95ae2a6..2bd8f59 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -171,6 +171,35 @@ package, and `raco rivet verify` behavior identical on all three platforms. Projects created before this feature remain valid because all three settings are optional. To embed a Windows icon in an older generated host, add Rivet's conditional `RIVET_WINDOWS_ICON_RC` `ResourceCompile` item from the current host template or regenerate the host project while preserving application UI sources. +## Application data directories + +Resources are read-only and ship with the application. Mutable user data — +documents, preferences, caches, databases — is written at runtime by the +product, and Rivet deliberately provides no API for it: the location follows +each platform's convention and stays under the product's control. + +- **Windows**: `%APPDATA%\\` (roaming; use `%LOCALAPPDATA%` + for large caches). +- **macOS**: `~/Library/Application Support/` (plus `~/Library/Caches/` + for purgeable caches). +- **Linux**: `$XDG_DATA_HOME/` when `XDG_DATA_HOME` is set, + otherwise `~/.local/share/`; caches under + `$XDG_CACHE_HOME` or `~/.cache`. + +Never write into the user's home root, the install directory, or +`dist/`/`build/` (Rivet-owned generated roots). The application identifier +from `rivet.rktd` is the conventional directory name. + +Products should keep one small, testable resolver for that directory and +honor an environment-variable override (for example `_DATA_DIR`), +so acceptance suites can inject disposable roots instead of touching a +developer's real profile — see [product acceptance evidence](product-acceptance.md) +and the native-UI launch orchestration, both of which rely on that hook. +Write files atomically (write to a temporary file in the same directory, +then rename), and load defensively: a truncated or corrupt file is a reason +to fall back to defaults (and surface a diagnostic), not to crash on +startup. + ## User-visible strings: shared/i18n Projects that show user-facing strings keep one flat single source: diff --git a/docs/native-ui-testing.md b/docs/native-ui-testing.md index 88fac90..74fbbcd 100644 --- a/docs/native-ui-testing.md +++ b/docs/native-ui-testing.md @@ -54,6 +54,9 @@ reaping the application around it. A proven shape for that runner: 1. `raco rivet package` produces the development distributable; locate the newest bundle under `dist/` and its executable with `plutil -extract CFBundleExecutable raw` on the bundle's `Info.plist`. + By default `package` also launches the artifact once as a launch smoke — + pass `--skip-launch-smoke` when the suite itself owns launching, so the + run stays hermetic. 2. Launch the executable in the background with a disposable data root in the environment (the application's own override hook — never the developer's real profile), and take `$!` as the pid for the driver. diff --git a/docs/product-acceptance.md b/docs/product-acceptance.md index 927990b..d212e9e 100644 --- a/docs/product-acceptance.md +++ b/docs/product-acceptance.md @@ -54,6 +54,8 @@ The supported suite kinds are: Suite ids are unique symbols. Platforms are a non-empty subset of `windows`, `macos`, and `linux`. A command is a non-empty list of argv strings; shell syntax, redirection, pipes, and command interpolation are not interpreted. +Every suite command runs with the project root as its current directory, so +paths in a suite command are relative to the project. Timeouts are positive integers capped at two hours. Evidence entries are file paths below `.rivet/acceptance/`. Rivet removes each declared file before the suite starts and then requires the suite to recreate it, so a stale artifact @@ -111,19 +113,34 @@ ordinary Racket module: requiring it runs the `define-*` declarations, and process can exercise a real embedded-backend round trip: 1. Instantiate the backend module under test (declarations register when the - module is instantiated); use a fresh namespace when a test needs a - "restarted" backend, and take `record-ref` from that same namespace. + module is instantiated). A test that needs a "restarted" backend must + re-instantiate the module under a fresh namespace, take `serve` (and any + `record-ref`) from that same namespace, and require the module by file + path — a quoted symbol resolves only in the namespace that already knows it: + + ```racket + (define ns (make-base-empty-namespace)) + (parameterize ([current-namespace ns]) + (namespace-require/copy `(file ,(path->string backend-path)))) + (define serve-proc + (parameterize ([current-namespace ns]) (eval #'serve))) + ``` 2. Build a pipe pair, run `(serve in out)` on a thread under a custodian, and remember that `serve` blocks until the session ends; closing the client's output port ends it. 3. The backend writes the hello frame at startup, before it reads anything, so the first `read-frame` on the client side observes it. -4. Speak RVT1 with `rivet/protocol`: `write-frame`, `read-frame`, and - `encode-value`/`decode-value` are public. There is no separate in-process - RPC entry point — the protocol boundary is the contract under test. +4. Speak RVT1 with `rivet/protocol`, which publicly exports the full testing + surface: `protocol-version`, the `frame` struct and its accessors, + `message:hello`, `message:request`, `message:response`, `message:error`, + `message:event`, `message:cancel`, `message:shutdown`, `write-frame`, + `read-frame`, `encode-value`, `decode-value`, and the limit constants. + There is no separate in-process RPC entry point — the protocol boundary is + the contract under test. 5. Assert wire-visible outcomes: the response payload for an accepted call, - an error frame for a rejected input, and the reserved `$state` Event that - a State mutation emits before the response frame. + an error frame for a rejected input (its payload is an encoded String + value), and the reserved `$state` Event that a State mutation emits before + the response frame. ```racket (require rivet/backend diff --git a/docs/protocol.md b/docs/protocol.md index 32547ea..5cbd650 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -107,7 +107,7 @@ Void procedures return the Null/Void value. `$state/get` and `$state/set` return ## Error -Errors associated with requests reuse the request id. The v1 error payload is a UTF-8 String containing a human-readable message. +Errors associated with requests reuse the request id. The v1 error payload is a UTF-8 String containing a human-readable message. On the wire that string is an RVT1-encoded String value — clients decode it with the value decoder (`decode-value` on Racket, the generated client's value decoding on native sides) and receive a plain string; the payload bytes are not the raw UTF-8 text. The Swift runtime surfaces a backend Error frame as `ClientError.backend(String)` from the generated calls, carrying that decoded message (with a generic fallback when the payload is not a String value). The Racket backend bounds generated Error diagnostics to 4096 Unicode characters. Longer exception or rejection messages are truncated and end with `[truncated]`. For admitted requests, Rivet encodes this bounded Error payload before competing with cancellation for terminal-response ownership, so an Error serialization failure cannot silently consume the request without sending a terminal frame. @@ -178,7 +178,8 @@ The Racket backend represents schema values as ordinary Racket data, validated a - `(Optional T)` uses `(void)` as the absent value — it is the only Racket value that encodes as Optional null. `'()` and `#f` are rejected for an Optional field even though `'()` is a legal empty `(List T)`; the representation is type-directed, not value-directed. - `define-record` generates a constructor named after the type — `(Task 1 "ship docs" 1761300000000)` for `(define-record Task ([id : Int64] [title : String] [created-at-ms : Int64]))`. Field access uses `(record-ref value 'title)` (the field name may be a symbol or a string, and is contract-checked). Records are opaque structs: `match` patterns and generated accessors do not exist, so use the constructor and `record-ref`. A record value is tied to the module instance that declared its type; a test that re-instantiates the backend module under a fresh namespace must also take `record-ref` from that namespace, because the predicate rejects values from another instance. - `define-enum` likewise generates a constructor named after the type taking the case (`(Priority "high")`), and `enum-case` reads the case back as a symbol. -- `define-state` binds a State descriptor; `state-ref` and `state-set!` read and write it, and `state-set!` validates and pre-encodes the `$state` Event before committing. `emit-event!` publishes a typed Event from anywhere in the backend. +- `define-state` binds a State descriptor; `state-ref` and `state-set!` read and write it, and `state-set!` validates and pre-encodes the `$state` Event before committing. `emit-event!` publishes a typed Event from anywhere in the backend. Calling `state-set!` before a session exists is safe: the cell commits and no Event is emitted (there is no transport to deliver it on yet), so initialization-time writes behave like ordinary mutation. +- `define-rpc` procedures take their declared arguments. A zero-argument RPC omits the argument list: `(define-rpc (ping : String) "pong")`. Its wire request payload is the single-element list `["ping"]`. `raco rivet build` regenerates the native clients from these declarations; the Racket forms above are the single source of truth for both sides.