Assert
YAML-driven test orchestration for hardware validation
Station Tools
Test Runner
Run tests, monitor live execution and step verdicts in real time
Test Results
This station's results, measurements and pass/fail trends
Packages
Discover and manage installed test packages and runner capabilities
YAML Validator
Validate and preview test definition YAML before deploying
Station Config
View and manage this station's runtime configuration
Central Services
APIs
AI Assistant (MCP)
Connect your AI assistant to this station
Select your AI client below for setup instructions.
Run this command once in your terminal to register this station with Claude CLI:
mcp add --transport http assert-demo-station https://api.demo.esharp.se:7004/mcp
Assert 2.1.0
What's new
2.1.0
2026-09-16
- ### The station is who it says it is (pg-isolation increment 5)
- The station mints an **ES256 keypair on first boot** (`ASSERT__IDENTITYPATH`, default `/data/identity`,
- its own volume in the compose files) and **signs every Orchestra request** with it — five
- `X-Assert-*` headers over the method, path, a timestamp, a nonce and the body hash. The private key
- never leaves the container.
- It **enrols itself** (`POST api/orchestra/stations/{id}/identity`) at start and every five minutes.
- Orchestra 1.22.0 pins the key on first sight; the same key is confirmed every time after. The
- answer is the station's identity state: `active`, `pending-approval` (a different key is already
- pinned for this name — someone trusts the new one in the Dashboard), `revoked`, or `unsupported`
- (Orchestra < 1.22.0). Visible in `/health/ready`, `GET /api/system/identity`, the UI banner and
- the station log.
- `ASSERT__INGESTKEY` still works and still travels (dual-accept), so upgrading changes nothing on
- day one; once every station shows `signature` at Orchestra the fleet key can be blanked.
2.0.0
2026-09-16
- ### Assert: the station has no database
- The station product is now **Assert**, in its own repository, and a station no longer holds a
- database connection. Every result it records and every report, statistic, artifact, configuration
- value and lock state it shows travels over HTTPS to the central Orchestra service. This is the end
- of the pg-isolation work (increments 1–4 shipped in 1.36.0; 5 remains, see Docs/PERSISTENCE.md).
- Breaking — deployment.** The one required setting is `ASSERT__CENTRALURL`. `ConnectionStrings__DefaultConnection`,
- `POSTGRES_*`, `Persistence__Mode`, `Persistence__IngestKey` and `Database__ApplyMigrationsOnStartup`
- are gone; the ingest key is `ASSERT__INGESTKEY`. The API refuses to start without the URL, and
- against an Orchestra older than **1.21.0** (the release that added the read half of the API). The
- `DirectDb` and `Dual` persistence modes no longer exist — a station that needs them is a Maestro 1.x
- station. Re-running `Start-Assert-Station.ps1` rewrites an old `.env`.
- Renamed.** Product strings, the MCP server key (`assert`) and tools (`assert_bug_report`,
- `assert_feature_request`, `assert_changelog`, `assert_error_log`), configuration prefix (`Assert__`),
- images (`ghcr.io/esharpab/assert/*`), compose project (`assert-station`) and containers (`assert-*`),
- and the identifiers that carried the old name. Namespaces stay `WorkflowEngine.*`:
- `WorkflowEngine.Contracts.dll` is what every test package is compiled against. Two wire spellings are
- deliberately pinned because the other side still reads them — the `X-Maestro-Ingest-Key` header
- (also sent as `X-Assert-Ingest-Key`) and the `maestroVersion` JSON property on the station status.
- Behaviour that changed with the store.**
- Statistics are computed by Orchestra; the station's `api/statistics/*` routes relay to it, same
- parameters, same shapes.
- `/health/ready` and `/health/detailed` report Orchestra reachability and version instead of a
- database; the UI banner says "Cannot reach Orchestra".
- Warning-and-above application events are shipped to Orchestra in batches and read back through
- `GET /api/system/error-log` / `assert_error_log`, scoped to this station.
- The MES retry queue lives in Orchestra (delivery attempts still run here). Station availability
- events, site assignment and the changelog table likewise.
- The merged station configuration is cached for two seconds and dropped on any write from this
- station; the edit page and single entries are never cached.
- Removed.** `TestDbContext` and all 37 migrations, the direct-DB repository and persistence sinks,
- `DbFeedbackStore`, the DB `DefinitionRegistry`, `PersistenceOptions`, `Licensing.Postgres`, the
- Npgsql and EF provider packages, and the vendored `ESharp.Access` feed nothing here consumed. The
- DB-layer tests moved to Orchestra's test suite along with the code they exercised.
- ## [version]` section is synced into
- the `maestro_system_releases` table on startup by `AssertChangelogSyncService` (idempotent, keyed
- by version) and surfaced via the `assert_changelog` MCP tool and `GET /api/system/releases`.
- When you bump `<Version>` in `WorkflowEngine.Api.csproj`, add a matching section here in the same
- commit. Format: `## [x.y.z] - YYYY-MM-DD` with an optional `(buildhash)` suffix.
1.56.0
2026-09-10
- ### A prompt step records what the operator actually answered (feature request #41)
- A prompt recorded its own verdict and nothing about what was chosen. Where the answer gates a
- `precondition:` on later steps, the only trace of the decision was which children came back
- SKIPPED. On two collimator executions all eight prompts passed and 41 steps were skipped, so 128
- measurements are absent from the record with nothing saying the operator chose to skip calibration.
- By that route a declined calibration and a calibration that vanished through a fault read
- identically, and an evidence roll-up cannot tell them apart.
- Every prompt step now records the chosen button as a string measurement named `Response`, with the
- log operator so it is informational and can never move a verdict. Nothing to declare and nothing to
- switch on.
- Unattended runs record their answer too, suffixed `[unattended]`, and a timeout auto-submit is
- suffixed `[timed out]`. A machine-chosen answer is a decision as much as a person's, and the
- record should not pass one off as the other.
- A test asserts the log operator cannot drag the verdict: a Fail button still fails the step while
- its response measurement passes.
- Not in this change, and noted in the request as a second idea: letting a step declare a dependency
- on a prior execution, so a warm re-verify run can point at the cold run whose calibration it relies
- on rather than the pairing being inferred from timestamps. That is a bigger design question.
- ### Optional MCP tool parameters are optional again (bug #28)
- `search_reports` described all seven of its filters as optional and listed all seven as required in
- its JSON schema, so a call omitting any of them was rejected by validation before it reached the
- server. The workaround people found was passing the literal string "null", which happens to coerce.
- That is an accident, not an interface, and a stricter parse later would have turned it into zero
- rows rather than an error.
- The cause is that a parameter without a C# default is emitted as required whatever its description
- says. It was not confined to the reported tool.
- | Tool | Parameters that were required by accident |
- |---|---|
- | `search_reports` | all seven |
- | `get_test_report_full`, `get_step_results` | page, pageSize |
- | `wait_for_package` | name, timeoutSeconds |
- | `wait_for_test_completion` | timeoutSeconds |
- | `start_test` | operatorId, unattendedMode |
- | `set_config_value` | category, description |
- | `set_package_registry_settings` | gitUsername, gitToken |
- Every one of those already had its intended default written into the method body or its
- description, so nothing about behaviour changes. Only the schema now agrees. Each also gained a
- per-parameter description while it was open.
- Three tests assert the cause rather than the symptom, over every tool found by reflection: a
- nullable parameter always has a default, paging parameters always have one, and `search_reports`
- has no required parameters at all. They fail three ways against the code as it was.
1.55.0
2026-09-10
- ### Plausibility bounds, so a contaminated history can be analysed at all (feature request #2)
- `invalid_value` is forward-only by construction. It helps samples recorded after somebody edits the
- YAML and re-runs, and it repairs nothing already in the database. The warning added in 1.54.x names
- the contamination but changes no figure, so on the station's own data the answer to "what is this
- rail's capability" was still a mean of -156 amperes and a negative Cpk. The data was unusable as it
- stood.
- `get_measurement_statistics` now takes `plausibleMin`, `plausibleMax` and `reason`. Samples outside
- the bounds leave mean, sigma, Cp and Cpk for that query only. Nothing is mutated and nothing is
- written.
- All four guardrails from the request are in:
- Never a default.** A query without the parameters returns the full population, byte for byte
- what it returned before.
- Full echo.** The bounds come back, `excludedImplausibleSamples` counts what went, and the note
- lists the removed values with their multiplicities.
- A must-show note.** `plausibilityNote` opens with THESE FIGURES ARE FILTERED. A capability index
- gets quoted in customer documents, and one computed under caller-chosen bounds is a different
- claim from one computed over everything recorded.
- An audit trail.** `reason` is echoed into the note. It is optional rather than required, because
- making it mandatory would block the exploratory query the request describes — but omitting it
- prints NO REASON WAS RECORDED, which travels with the figure just as loudly.
- The framing the request asked for is enforced in the docs and the tool description rather than in
- code, because it cannot be enforced in code: ask whether a value is physically possible for the
- declared unit and spec, never whether it is inconvenient. A test asserts the distinction with the
- two real cases — a -999 A reading on a 1 A rail is removed by bounds of 0 to 10, and a -100 % gain
- error is KEPT by bounds of -100 to 100, because percent is bounded at -100 and that is physics
- rather than preference.
- ### The capability scan says where to point bounds, and deliberately does not take them
- The request also asked for bounds on `get_capability_summary`. I have not done that, because one
- pair of numbers cannot be physically meaningful across a scan that mixes amps, hertz and percent:
- 999 is impossible for a one amp rail and unremarkable for a temperature offset. Bounds are
- per-measurement physics, and a global pair would be the "trim until it looks acceptable" failure the
- request explicitly warns against, wearing the name that was chosen to prevent it.
- Each row instead carries `suspectedSentinelNote` when its Cpk looks bad for a reason that is not the
- process, using the same detector as the measurement endpoint. The scan finds the measurements worth
- investigating; the measurement endpoint is where the bounds go. Ready to be overruled if the
- requester wants the parameters on the scan anyway.
1.54.1
2026-09-09
- ### The sentinel warning missed the case it was written for, and fired on a healthy one
- Reproduced on the reporting station before changing anything, which is what found this. Queried
- exactly as bug #11 queries it, `PHOENIX_CH0_IMEAS_A` on 1.54.0 returned mean -156.27 against a 0.95
- to 1.05 A specification, `excludedInvalidSamples: 0`, `excludedNonMeasurementSamples: 0`, and no
- data-quality note at all. The warning did not fire on the population the ticket is about.
- Two things were wrong, both found in the same data.
- The share threshold was the bug.** The rule wanted one value to be at least 20 % of the
- population. Five sentinels among 38 pooled samples is 13 %, so the pooled query said nothing.
- Narrowed to one step it was 83 % and the warning fired correctly, which is how the miss was
- visible at all: the same measurement warned or stayed silent depending on how it was sliced.
- Share is gone.
- Repetition alone is not the signature.** On the step next door the same measurement records
- 1.931457519531 three times out of six, and the old rule called it a suspected sentinel. It is a
- real reading: a stable load through an ADC lands on the same code repeatedly. A warning that
- fires on healthy data is worse than no warning, because it teaches people to skip it.
- The rule now requires BOTH repetition and gross distance, and distance is measured in specification
- widths so the threshold means the same thing for a 0.1 A window and a 400 Hz one. On the station's
- own data the two populations are nowhere near each other:
- | Sample | Widths outside the limits | Warns |
- |---|---|---|
- | A real, stable ADC reading that repeats | 9 | no |
- | A dead channel at -100 % gain error | 9.5 | no |
- | A zero-valued sentinel on a frequency sweep | 24 | yes |
- | The reported -999 A sentinel | 10,000 | yes |
- Each signal alone gets one of these wrong, and the two it gets wrong are the samples an engineer
- must not be trained to ignore. Those five rows are now a data-driven test rather than a comment.
- Still true, and still the point: it excludes nothing. A channel genuinely stuck at one impossible
- value looks identical from here, and that is a finding.
- Two limits on the warning are now documented rather than implied. It needs both limits and a
- non-zero window, so a measurement whose current definition has no limits gets no warning even
- though its mean can still be dragged. And it describes the population you asked for, so a pooled
- query that says nothing is worth re-running per step.