> ## Documentation Index
> Fetch the complete documentation index at: https://docs.roboticks.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Wire Contract Reference

> Authoritative wire-contract reference for the Roboticks SDK - schema versioning, JUnit XSD, test-result JSON Schema, forward-compat rules.

# Wire contract reference

The wire between the Roboticks SDK and the Roboticks platform is **JUnit XML extended with `roboticks.*` properties**, converted server-side to a **canonical JSON document**. This page is the authoritative reference. For the tutorial introduction, see [Testing → Wire contract](/testing/wire-contract).

```text theme={null}
github.com/roboticks-io/roboticks-sdk
└── schemas/
    ├── junit_with_confirms.xsd       # XML schema for the upload format
    ├── test_result.schema.json       # JSON schema for the parsed shape
    └── requirement.schema.json       # JSON schema for requirement uploads
```

## Schema version

The wire contract is versioned as a single integer. Currently `2`.

```python theme={null}
from roboticks._version import SCHEMA_VERSION
print(SCHEMA_VERSION)  # 2
```

### Version policy

| Increment                                     | Cause                                                                                                             |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Major** (`1 → 2`)                           | Any change that a schema-1 reader cannot parse. New required properties; renamed properties; changed value types. |
| **Add property** (no bump)                    | New optional `roboticks.*` property. Older readers ignore unknown keys.                                           |
| **Remove property** (no bump for one release) | Property is deprecated but still emitted; readers tolerant. Next major removes the emission.                      |

### v2 changes vs v1

* **Added** (required when emitted): `roboticks.nodeid` on every `<testcase>` — the pytest nodeid, used by the platform to deterministically route per-test-case artifacts into the S3 prefix `test-runs/{run_id}/test-cases/{sha256(nodeid)[:16]}/`.
* **Added** (optional, repeatable): `roboticks.attach.{kind}` — one property per file registered via `roboticks.attach_artifact()`. The property *name* carries the kind (`mcap`, `logs`, `attachments`, or any custom single-segment label) and the *value* is the local file path; the runner uploader walks these and posts each file to the per-test-case prefix.
* **Behavior**: schema-1 uploads still parse — the platform falls back to a run-level prefix when `roboticks.nodeid` is absent. Schema-2 readers tolerate the absence of `roboticks.attach.*` (it just means the test did not call `attach_artifact`).

## Suite-level properties

| Property                   | Value                 | Required                  | Since |
| -------------------------- | --------------------- | ------------------------- | ----- |
| `roboticks_schema_version` | integer               | yes                       | 1     |
| `roboticks.sdk.version`    | semver string         | yes                       | 1     |
| `roboticks.sdk.language`   | `"python"` or `"cpp"` | yes                       | 1     |
| `roboticks.python.version` | semver string         | conditional (Python only) | 1     |
| `roboticks.cpp.version`    | semver string         | conditional (C++ only)    | 1     |

## Test-case properties

| Property                           | Value format                                                   | Required             | Since | Decorator / source                      |
| ---------------------------------- | -------------------------------------------------------------- | -------------------- | ----- | --------------------------------------- |
| `roboticks.nodeid`                 | pytest nodeid string (`pkg/test_x.py::TestCls::test_y[param]`) | yes (schema ≥ 2)     | 2     | plugin (stamped on every `<testcase>`)  |
| `roboticks.confirms`               | comma list of requirement IDs                                  | conditional          | 1     | `@confirms`                             |
| `roboticks.tags`                   | comma list of strings                                          | optional             | 1     | `@tags`                                 |
| `roboticks.deadline_ms`            | integer                                                        | optional             | 1     | `@deadline`                             |
| `roboticks.requires_sim`           | `engine` or `engine:gpu`                                       | optional             | 1     | `@requires_sim`                         |
| `roboticks.fault_injection`        | JSON array                                                     | optional             | 1     | (inferred from `fault_injection` usage) |
| `roboticks.mcap.path`              | string (relative path)                                         | optional             | 1     | (set by `mcap_capture`)                 |
| `roboticks.mcap.uploaded`          | `"true"` or `"false"`                                          | optional             | 1     | (set by runner after upload)            |
| `roboticks.coverage.lines.covered` | integer                                                        | optional             | 1     | (set by coverage post-processor)        |
| `roboticks.coverage.lines.total`   | integer                                                        | optional             | 1     | (set by coverage post-processor)        |
| `roboticks.attach.{kind}`          | string (local file path)                                       | optional, repeatable | 2     | `attach_artifact(path, kind=...)`       |

### `roboticks.attach.{kind}` — per-test artifact attachments

Schema 2 adds a property family for files a test registers via [`attach_artifact()`](/sdk/overview#attach_artifact). The property *name* carries the kind, the *value* carries the local path:

```xml theme={null}
<property name="roboticks.attach.mcap" value="/tmp/run_42/test_estop.mcap"/>
<property name="roboticks.attach.attachments" value="/tmp/run_42/before.png"/>
<property name="roboticks.attach.attachments" value="/tmp/run_42/after.png"/>
<property name="roboticks.attach.logs" value="/tmp/run_42/decision_log.jsonl"/>
```

Reserved kinds and their conventional sub-folders (the runner uploader follows these when it lays out S3):

| Kind          | Conventional use                                             |
| ------------- | ------------------------------------------------------------ |
| `mcap`        | MCAP bag files                                               |
| `logs`        | Plain-text or JSONL log dumps                                |
| `attachments` | Screenshots, generated reports, anything else                |
| *(custom)*    | Any single URL-safe path segment, e.g. `traces`, `coredumps` |

The runner walks `<testcase>` for every property whose name starts with `roboticks.attach.` and posts the file to the platform's `/test-runs/{run_id}/mcap-upload-urls` endpoint with `{nodeid, kind}` in the body. The backend lands it at:

```text theme={null}
test-runs/{run_id}/test-cases/{sha256(nodeid)[:16]}/{kind}/{filename}
```

so the workspace UI groups artifacts under the test that produced them.

## JUnit XSD excerpt

```xml theme={null}
<!-- schemas/junit_with_confirms.xsd -->
<xs:complexType name="testcaseType">
  <xs:sequence>
    <xs:element name="properties" minOccurs="0">
      <xs:complexType>
        <xs:sequence>
          <xs:element name="property" maxOccurs="unbounded">
            <xs:complexType>
              <xs:attribute name="name" type="xs:string" use="required"/>
              <xs:attribute name="value" type="xs:string" use="required"/>
            </xs:complexType>
          </xs:element>
        </xs:sequence>
      </xs:complexType>
    </xs:element>
    <xs:choice minOccurs="0">
      <xs:element name="failure" type="xs:string"/>
      <xs:element name="error" type="xs:string"/>
      <xs:element name="skipped" type="xs:string"/>
    </xs:choice>
  </xs:sequence>
  <xs:attribute name="name" type="xs:string" use="required"/>
  <xs:attribute name="classname" type="xs:string" use="required"/>
  <xs:attribute name="time" type="xs:decimal" use="required"/>
</xs:complexType>
```

The full XSD is shipped at `schemas/junit_with_confirms.xsd` in the [SDK repo](https://github.com/roboticks-io/roboticks-sdk/blob/main/schemas/junit_with_confirms.xsd). Validate locally:

```bash theme={null}
xmllint --schema schemas/junit_with_confirms.xsd test_results/junit.xml --noout
```

## Canonical test-result JSON

The platform converts JUnit-with-confirms into this JSON shape:

```json theme={null}
{
  "schema_version": 2,
  "test_id": "tests.test_estop::test_estop_halts_motion",
  "nodeid": "tests/test_estop.py::test_estop_halts_motion",
  "nodeid_slug": "a4f3b9c218e07d54",
  "suite": "tests.test_estop",
  "name": "test_estop_halts_motion",
  "result": "passed",
  "duration_ms": 82,
  "confirms": ["REQ-001", "REQ-014"],
  "tags": ["safety", "smoke"],
  "deadline_ms": 100,
  "requires_sim": null,
  "fault_injection": [],
  "artifacts": {
    "mcap": null,
    "stdout_url": "s3://...",
    "stderr_url": "s3://...",
    "attachments": [
      {"kind": "mcap",        "key": "test-runs/.../test-cases/a4f3b9c2.../mcap/test_estop.mcap"},
      {"kind": "attachments", "key": "test-runs/.../test-cases/a4f3b9c2.../attachments/before.png"}
    ]
  },
  "coverage": {
    "lines_covered": 17,
    "lines_total": 19
  },
  "sdk": {
    "language": "python",
    "version": "0.2.0a0"
  }
}
```

`nodeid_slug` is `sha256(nodeid).hexdigest()[:16]` — derived deterministically on both the SDK and the platform so the per-test-case S3 prefix is stable across re-runs without round-tripping the slug through the wire.

JSON Schema (excerpt):

```json theme={null}
{
  "$id": "https://schemas.roboticks.io/test_result.schema.json",
  "type": "object",
  "required": ["schema_version", "test_id", "nodeid", "result", "duration_ms", "confirms"],
  "properties": {
    "schema_version": {"type": "integer", "const": 2},
    "test_id": {"type": "string"},
    "nodeid": {"type": "string"},
    "nodeid_slug": {"type": "string", "pattern": "^[0-9a-f]{16}$"},
    "result": {"enum": ["passed", "failed", "error", "skipped"]},
    "duration_ms": {"type": "integer", "minimum": 0},
    "confirms": {"type": "array", "items": {"type": "string"}},
    "deadline_ms": {"type": ["integer", "null"], "minimum": 1},
    "requires_sim": {"type": ["string", "null"]},
    "artifacts": {
      "type": "object",
      "properties": {
        "attachments": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["kind", "key"],
            "properties": {
              "kind": {"type": "string"},
              "key": {"type": "string"}
            }
          }
        }
      }
    }
  }
}
```

Full schema: `schemas/test_result.schema.json`.

## Forward / backward compatibility

| Scenario                                                       | Platform behaviour                                                                                                                                         |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Upload has no `roboticks_schema_version`                       | Treat as stock JUnit. No requirement linking. Tests still show pass/fail.                                                                                  |
| Upload has `roboticks_schema_version = 1`, platform supports 2 | Parse via the v1 legacy adapter. Per-test-case artifacts land under the run-level `test-runs/{run_id}/mcaps/` prefix (no nodeid → no per-case sub-folder). |
| Upload has `roboticks_schema_version = 2`, platform supports 2 | Parse normally. Per-test-case artifacts land under `test-runs/{run_id}/test-cases/{slug}/`.                                                                |
| Upload has `roboticks_schema_version = 3`, platform supports 2 | **Warn** on the Check Run; drop unknown properties; parse the v2 subset.                                                                                   |
| Upload has `roboticks_schema_version = 0`                      | **Reject** with a clear error pointing at the SDK upgrade path.                                                                                            |

The platform supports the **current and one-back** schema versions (v2 and v1 today).

## Disagreement handshake

When the SDK and the platform disagree on schema version, the platform writes a `roboticks.platform.warnings` element into the run's metadata:

```json theme={null}
{
  "platform_schema_version": 2,
  "sdk_schema_version": 3,
  "unknown_properties_dropped": ["roboticks.new_thing"],
  "deprecated_properties_seen": []
}
```

This element is surfaced in the Check Run as a one-line warning and in the run-detail UI as a banner.

## What the SDK guarantees

* A given SDK release pins to exactly one schema version.
* Within a major SDK series (e.g. all `0.x.y`), the schema version doesn't decrease.
* The XSD and JSON Schema files in the SDK repo at a tag are the authoritative artefacts for that release.

## What the platform guarantees

* Backwards-compat for at least the current and one-back schema versions.
* Forward-tolerance: unknown `roboticks.*` properties are dropped, not rejected.
* The conversion from XML to canonical JSON is documented and stable per schema version.

## Tools

```bash theme={null}
# Validate a JUnit XML against the XSD
roboticks-validate-junit test_results/junit.xml

# Validate a JSON result against the JSON Schema
roboticks-validate-json result.json

# Inspect schema version of a test result file
roboticks-inspect test_results/junit.xml
# > schema_version: 2
# > sdk: roboticks 0.2.0a0 (python 3.13.1)
# > tests: 12 (12 pass, 0 fail)
# > confirms: 18 requirement IDs across 12 tests
# > attachments: 4 files across 3 testcases (mcap=2, attachments=2)
```

## Next

<CardGroup cols={2}>
  <Card title="Wire contract tutorial" icon="file-code" href="/testing/wire-contract">
    The version-handshake story in narrative form.
  </Card>

  <Card title="Pytest plugin" icon="puzzle-piece" href="/sdk/pytest-plugin">
    Reference for the writer.
  </Card>

  <Card title="C++ reference" icon="code" href="/sdk/cpp-reference">
    Reference for the C++ writer.
  </Card>

  <Card title="Schemas in the SDK repo" icon="github" href="https://github.com/roboticks-io/roboticks-sdk/tree/main/schemas">
    Source of truth.
  </Card>
</CardGroup>
