# MTConnect integration-test fixture — the official C++ Agent + an SHDR data source

The MTConnect C++ Agent (`mtconnect/agent`, pinned) serving a canned device model, fed live
data by a standard-library SHDR adapter. No image build step: both services run stock images
with this folder's files bind-mounted.

> **The published image is `mtconnect/agent`, not `mtconnect/cppagent`.** `cppagent` is the
> name of the source project on GitHub; there is no Docker Hub repository under that name.

| File | Purpose |
|---|---|
| [`docker-compose.yml`](docker-compose.yml) | Two services: `agent` (published on :5000) and `adapter` (internal, :7878) |
| [`agent.cfg`](agent.cfg) | Agent configuration, bind-mounted at `/mtconnect/config/agent.cfg` (the image's CMD path) |
| [`Devices.xml`](Devices.xml) | The canned device model the Agent serves from `/probe` |
| [`adapter.py`](adapter.py) | SHDR feeder — the data source. Pure stdlib, runs on a stock `python:*-alpine` |

## Why there is an adapter service

The `mtconnect/agent` image ships **only** the agent binary plus its schemas and styles — there
is no bundled simulator. An Agent with no adapter answers `/probe` correctly and then reports
**every observation `UNAVAILABLE` forever**, which cannot prove that reads return real values or
that the `/sample` long poll delivers anything. `adapter.py` is what makes the fixture live.

The Agent dials **out** to the adapter (see the `Adapters` block in `agent.cfg`); the adapter is
not published to the host.

## Run

From the shared Docker host (stack dir `/opt/otopcua-mtconnect`):

```bash
docker compose up -d --wait
docker compose logs -f agent
docker compose down
```

From a dev box via the helper (see CLAUDE.md "Docker Workflow"):

```powershell
lmxopcua-fix sync mtconnect     # push this folder to /opt/otopcua-mtconnect/
lmxopcua-fix up   mtconnect     # single-service-shaped stack, no profile argument
lmxopcua-fix logs mtconnect
lmxopcua-fix down mtconnect
```

### Running it on a Mac

macOS **squats port 5000** — AirPlay Receiver (ControlCenter) binds `*:5000` and wins the race,
so `docker port` reports a healthy publish while every request answers `403 Forbidden` with
`Server: AirTunes`. Use the host-port override:

```bash
MTCONNECT_AGENT_HOST_PORT=5555 docker compose up -d --wait
MTCONNECT_AGENT_ENDPOINT=http://127.0.0.1:5555 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests
```

## Endpoint

- Default: `http://10.100.0.35:5000` (the shared Docker host; 5000 is the Agent's own default port).
- Override with `MTCONNECT_AGENT_ENDPOINT` to point at a real Agent on a machine tool.
- `MTCONNECT_AGENT_HOST_PORT` changes only the **published host port** of the fixture container.

`MTConnectAgentFixture` issues one `GET {endpoint}/probe` at collection init and records a
`SkipReason` when it fails, so the suite skips cleanly on a box with no fixture running.

## The seeded device model

Every named DataItem deliberately has `name != id` — the inverse of the driver's hand-authored
unit fixtures, and the only arrangement under which confusing the browse name with the
observation correlation key is visible.

| DataItem `id` | `name` | Category | Type | Repr. | Inferred type |
|---|---|---|---|---|---|
| `fixture_avail` | `Favail` | EVENT | AVAILABILITY | | String |
| `fixture_x_pos` | `Xact` | SAMPLE | POSITION (ACTUAL, MILLIMETER) | | Float64 — **moves** |
| `fixture_x_load` | `Xload` | SAMPLE | LOAD (PERCENT) | | Float64 — **moves** |
| `fixture_x_travel` | `Xtravel` | CONDITION | POSITION | | String + IsAlarm |
| `fixture_c_speed` | `Cspeed` | SAMPLE | ROTARY_VELOCITY (REVOLUTION/MINUTE) | | Float64 — **moves** |
| `fixture_c_temp_series` | `Ctemps` | SAMPLE | TEMPERATURE (CELSIUS) | TIME_SERIES | Float64 **array**, ArrayDim `null` |
| `fixture_mode` | `Cmode` | EVENT | CONTROLLER_MODE | | String |
| `fixture_execution` | `Pexec` | EVENT | EXECUTION | | String |
| `fixture_partcount` | `Pcount` | EVENT | **PART_COUNT** | | **Int64** — the regression case |
| `fixture_linenumber` | `Pline` | EVENT | LINE_NUMBER | | Int64 |
| `fixture_program` | `Pprogram` | EVENT | PROGRAM | | String |
| `fixture_block` | `Pblock` | EVENT | BLOCK | | String — **never fed ⇒ UNAVAILABLE** |
| `fixture_varset` | `Pvars` | EVENT | VARIABLE | DATA_SET | String (structured ⇒ BadNotSupported) |
| `fixture_logic` | `Plogic` | CONDITION | LOGIC_PROGRAM | | String + IsAlarm |
| `fixture_asset_changed` / `fixture_asset_removed` | — | EVENT | ASSET_CHANGED / ASSET_REMOVED | | String |

The Agent additionally injects **its own `<Agent>` self-model device** (connection status,
observation update rate, adapter URI). That is normal for every MTConnect 2.x Agent and the test
suite excludes it from value-plane assertions — its update-rate samples tick whether or not any
adapter is attached, so including them would let "the stream delivered a changed value" pass
against an Agent with no data source at all.

## Two things a real Agent taught us (both cost a fixture restart to find)

1. **`agent.cfg` must be pure ASCII.** A single non-ASCII byte — even inside a comment — makes
   the config parser reject the whole file with a bare `Failed / Stopped at line: N` and exit.
2. **`sampleCount` is not a DataItem attribute.** It belongs to a TIME_SERIES *observation*. An
   Agent that meets it on a declaration logs
   `The following keys were present and not expected: sampleCount` followed by
   `DataItems: Invalid element 'DataItem'` and **drops the entire data item** from the device
   model. `MTConnectDataTypeInference` therefore only ever sees `sampleCount = null` from a real
   Agent, so a live TIME_SERIES tag is always a variable-length array.
