> ## 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 (Testing)

> The JUnit-with-confirms wire format the SDK emits and the Roboticks platform ingests. Schema version handshake and forward-compat rules.

# Wire contract

The wire between the SDK and the Roboticks platform is **JUnit XML extended with `roboticks.*` properties**. Nothing else is required — no custom upload protocol, no proprietary metadata. If your test runner emits JUnit, it can speak Roboticks.

<Info>
  This page is a tutorial. For the authoritative schema (XSD + JSON Schema), see the [SDK wire contract reference](/sdk/wire-contract).
</Info>

## What the SDK emits

For a single Python test (schema version 2):

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="tests" tests="1" failures="0" errors="0" skipped="0" time="0.082">
    <properties>
      <property name="roboticks_schema_version" value="2"/>
      <property name="roboticks.sdk.version" value="0.2.0a0"/>
      <property name="roboticks.python.version" value="3.13.1"/>
    </properties>
    <testcase name="test_estop_halts_motion"
              classname="tests.test_estop"
              time="0.082">
      <properties>
        <property name="roboticks.nodeid" value="tests/test_estop.py::test_estop_halts_motion"/>
        <property name="roboticks.confirms" value="REQ-001,REQ-014"/>
        <property name="roboticks.tags" value="safety,smoke"/>
        <property name="roboticks.deadline_ms" value="100"/>
        <property name="roboticks.attach.mcap" value="/tmp/run/test_estop.mcap"/>
        <property name="roboticks.attach.attachments" value="/tmp/run/before.png"/>
      </properties>
    </testcase>
  </testsuite>
</testsuites>
```

Four observations:

1. **Schema version sits on the `<testsuite>`.** Per-testcase properties carry the semantic payload; suite-level properties carry the metadata about the *emitter*.
2. **`roboticks.confirms` is comma-separated.** Standard JUnit properties are stringly-typed; a single comma-list is more portable than nested elements.
3. **`roboticks.nodeid`** (added in v2) is what the platform uses to lay out per-test-case S3 folders. The hash slug it derives — `sha256(nodeid)[:16]` — becomes the sub-directory under `test-runs/{run_id}/test-cases/`.
4. **`roboticks.attach.{kind}`** (added in v2) is repeatable — one property per file registered with `attach_artifact()`. The runner uploader walks every `roboticks.attach.*` property and posts each file to the per-test-case S3 prefix.

## The properties

| Property                    | Scope    | Format                   | Meaning                                                                                              |
| --------------------------- | -------- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `roboticks_schema_version`  | suite    | integer                  | Schema version this XML conforms to. Currently `2`.                                                  |
| `roboticks.sdk.version`     | suite    | semver                   | SDK version that produced the XML.                                                                   |
| `roboticks.python.version`  | suite    | semver                   | Interpreter version (Python tests only).                                                             |
| `roboticks.nodeid`          | testcase | pytest nodeid            | Stable identity for the test. Drives the per-test-case S3 prefix. (v2+)                              |
| `roboticks.confirms`        | testcase | comma list               | Requirement IDs this test confirms.                                                                  |
| `roboticks.tags`            | testcase | comma list               | Free-form tags from `@tags(...)`.                                                                    |
| `roboticks.deadline_ms`     | testcase | integer                  | Deadline in ms from `@deadline(...)`.                                                                |
| `roboticks.requires_sim`    | testcase | `engine` or `engine:gpu` | Sim requirement from `@requires_sim(...)`.                                                           |
| `roboticks.fault_injection` | testcase | JSON                     | Fault primitives invoked, with topic + params.                                                       |
| `roboticks.mcap.path`       | testcase | string                   | Relative path to MCAP file if `mcap_capture` was used.                                               |
| `roboticks.attach.{kind}`   | testcase | string (local path)      | Repeatable; one per file registered via `attach_artifact()`. Kind goes into the S3 sub-folder. (v2+) |

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

## Per-test-case S3 layout (v2)

When the runner uploads artifacts, schema-2 metadata lets the platform fan files out into per-test-case sub-folders inside the run's S3 prefix:

```text theme={null}
test-runs/{run_id}/
├── junit/{junit.xml}                            # one per upload
├── mcaps/...                                    # run-level fallback (v1 + un-keyed)
└── test-cases/{sha256(nodeid)[:16]}/
    ├── mcap/test_estop.mcap
    ├── logs/decision_log.jsonl
    └── attachments/before.png
                       after.png
```

The 16-hex slug is computed deterministically on both the SDK and the platform from `roboticks.nodeid`; no round-trip is required. The workspace UI groups files under whichever test produced them, and `rbtk test files --nodeid` / `rbtk test cases` let you scope a download to a single test.

## The test-result JSON

For systems that prefer JSON to XML (the platform's internal store, the LLM triage prompt context, the matrix API), the platform converts JUnit-with-confirms into a JSON shape defined at `schemas/test_result.schema.json`:

```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",
  "result": "passed",
  "duration_ms": 82,
  "confirms": ["REQ-001", "REQ-014"],
  "tags": ["safety", "smoke"],
  "deadline_ms": 100,
  "requires_sim": null,
  "artifacts": {
    "mcap": null,
    "stdout": "s3://...",
    "stderr": "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"}
    ]
  }
}
```

The conversion is lossless. JSON is the canonical shape for everything *after* ingestion.

## Schema versioning

Schema versions are integers. Every increment is a **breaking change** to either the XML or the JSON shape. The platform supports the current version and one back.

| Version | Status             | Notes                                                                                                                   |
| ------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| 2       | **current**        | Adds `roboticks.nodeid` (required) + `roboticks.attach.{kind}` (optional, repeatable). Enables per-test-case S3 layout. |
| 1       | supported (legacy) | Initial GA. Still accepted; per-test-case artifacts fall back to the run-level prefix.                                  |
| 0       | rejected           | The pre-GA testing builds; never produced by a released SDK                                                             |

## The handshake

When the platform parses an uploaded JUnit XML, the first thing it does is read `roboticks_schema_version` from the suite properties:

```mermaid theme={null}
%%{init: {"theme": "neutral", "themeVariables": {"primaryColor": "#4040ff"}} }%%
flowchart TD
    Up["Upload JUnit XML"] --> R{Read<br/>roboticks_schema_version}
    R -->|absent| Stock["Treat as stock JUnit<br/>(no confirms, no requirement linking)"]
    R -->|matches platform| OK["Parse and link"]
    R -->|N-1| OK2["Parse via legacy adapter"]
    R -->|N+1| Warn["Warn in Check Run<br/>Drop unknown properties<br/>Parse known ones"]
```

The forward-compat behaviour matters: a newer SDK MAY upload a newer schema; the platform tolerates **unknown `roboticks.*` properties** rather than failing. You can upgrade the SDK ahead of a platform release without breaking your pipeline.

Conversely, the platform never downgrades. A schema-1 platform will not produce schema-0 JSON.

## What if I'm using a non-Roboticks test framework

If you emit stock JUnit (without `roboticks_schema_version`), the platform still ingests the file. You get:

* Test pass/fail in the Check Run.
* No requirement linking (the matrix shows the test under "Unlinked").
* No deadline, no tags, no MCAP correlation.

To get the full traceability story, you need *some* mechanism to attach `@confirms` to each test. Even a `<property name="roboticks.confirms" value="REQ-001"/>` written by hand into the JUnit XML works. The SDK is the convenient path, not the only path.

## Next

<CardGroup cols={2}>
  <Card title="SDK wire contract reference" icon="book" href="/sdk/wire-contract">
    Authoritative schema with XSD and JSON Schema files.
  </Card>

  <Card title="Pytest plugin internals" icon="python" href="/sdk/pytest-plugin">
    How the SDK actually writes those properties.
  </Card>

  <Card title="CI recipes" icon="gear" href="/testing/ci-recipes">
    How to get the XML to the platform.
  </Card>

  <Card title="Matrix UI" icon="table-cells" href="/traceability/matrix">
    Where the parsed `confirms` show up.
  </Card>
</CardGroup>
