# plesty init field-test Adds a field test — the gate sequence run against the real instrument — to an **existing** device module. Unlike [`plesty init mock-test`](init-mock-test.md), it interviews you. ```bash cd plesty-pm100d plesty init field-test # interactive plesty init field-test --force # replace an existing tests/field_test.py ``` `plesty.lib.test.field_test.FieldTestPipeline` gives every module the same gate sequence. The gates are the easy part — they are generic and already shared. What takes the time, and what three modules each got wrong at least once, is the **configuration**, because every line of it is a judgement about the instrument that no schema inspection can make: - which operations are safe to call unattended — the wrong answer moved a stage, fired an exposure, and rewrote a sensor's zero calibration - which parameters survive being written back their own value — one crashed the controlling application outright, killing every call after it - what is slow enough to be timed out on demand, since a call returning in two milliseconds cannot be abandoned and its gate silently skips - which two parameters are distinguishable enough that a stale reply is unambiguous - whether connecting is cheap, or starts an application and costs half a minute ## Candidates from the device, judgement from you So the command asks the **device** for the *candidates*, and you for the *judgement*. It constructs the module hardware-free — exactly as `plesty docs` does, which gate d3 already requires — and reads `doc_model()`. Not the JSON schema files, and the difference is not cosmetic. A module registers parameters and operations in Python too (`register_config` in `__init__`, an `@expose_to_api` method), and none of that is in a file: `plesty-pm100d` alone has two such operations, which a file-reading generator would never offer. And for a grouped schema the keys differ — `get_config_list()` answers `group_prefix.param_key` while the file is keyed by the bare name, so an `only_keys` list built from the file would name parameters the pipeline cannot resolve. That is the group-name against command-prefix mismatch three modules hit independently. There is no fallback to the files. A device module that cannot build its doc model already fails gate d3, and generating from a partial picture would produce a file that looks complete and tests less than it claims. ## The interview Each question states what the answer is used for before offering the choices, and each answer is followed by "why?" — the reason is written into the generated file as a comment, so the next reader sees why and not only what. Choices are a checkable list: arrow keys move, space selects, `a` selects all, `n` clears, Enter confirms, Escape backs out. Each entry shows what it is — a parameter's type and range, an operation's description — because choosing which operations may run unsupervised from a list of bare names is how the wrong one gets picked. Nothing is ticked to begin with, and Enter confirms whatever *is* ticked, so the cheapest keypress gives the most conservative answer. `a` exists but has to be typed. While a list is empty it says what empty means **for that question**, which is not the same everywhere: no operation selected means none is called, while no parameter selected means every one is written back. A run with no keyboard — piped input, a script, CI — falls back to typing comma-separated numbers rather than waiting for a keypress that never comes. **Nothing unclassified is treated as safe.** An operation you do not select is in neither list: it is never called, and the gate that would have called it skips saying so. A generated allow-list that quietly included a motion command would be worse than no generator at all. **What the module declares is not asked again.** An operation exposed with a kind — `@expose_to_api(kind="motion", position_key="MO.Position", step=1.0)`, `kind="lifecycle"`, `kind="control"`, or a `"kind"` block in `schema_func.json` (plesty-lib#39) — is shown as `[motion]`, `[lifecycle]`, `[control]` in the chooser, and the "acts on the world" question is asked only about operations that declared nothing. A `configure` operation is not offered at all: it is recorded as excluded, because a setting that outlives the call is verified by the round-trip gate. For an allow-listed motion the interview confirms how far the test may move it (`step`; the declared value is the default) and, if the module left it open, which parameter reports the position — a motion with neither is dropped, since the pipeline drives a motion one step out from that parameter and back and will not guess how far. A motion chosen as the slow operation is asked only how far the long move goes: the pipeline starts it from wherever the part is and moves it back afterwards. Both generated files carry the result as `OP_KINDS`. **An operation can be given the arguments it needs.** For each allow-listed operation that declares inputs (motions aside), the interview asks for arguments as `name=value` pairs — `device=LED1, voltage=0.5`. Each value is read as YAML, so `0.5` is a number and `LED1` a string. Enter gives none, and the gate generates every argument from its declared type and range, which suits a count or a voltage but not an argument that has to name something the device registered. Both generated files carry the answer as `OP_PAYLOADS` and pass it to their tier. ## The generated files The result is two files from one interview. `tests/field_test.py` runs on the bench against the instrument: hardware-gated (it defines no pytest test functions, so CI collects nothing), wired to publish `reports//field-test.json`, and runnable against the simulator first: ```bash uv run python tests/field_test.py --address mock uv run python tests/field_test.py --address
``` `tests/field_test_client.py` runs the same checks over the wire, against the served module. It inherits the answers rather than asking again: an operation unsafe on the bench is unsafe over the network too, and a second allow-list would be a second chance to disagree. Neither file is replaced without `--force`: they hold answers somebody thought about, and a hardware run may since have corrected them. ## The answers are kept Each answer is written to `.plesty/field-test.json` as it is given, and every run of the command starts from what that file holds — so Ctrl-C loses nothing, and running `plesty init field-test` again carries on from where the interview stopped. A question that was displayed but not answered is asked again; it is marked done only once an answer is in hand. The file is the source of truth: edit it to change an answer without repeating the interview, because an answer hand-edited in the generated `.py` is overwritten by the next run. Answers left by an older SDK in `.plesty-field-test.partial.json` are moved into `.plesty/` on the first run. The run ends by saying which constant holds which answer. The interview text ships in the wheel (`plesty/sdk/assets/init/field-test-questions.yaml`) and the generated file's skeleton alongside it (`field-test.py.template`).