# TEST-04 shared-contract experiment

This fixture supports the planned article *Shared Contract Tests Make
Substitutability Measurable*. It runs one unchanged consumer-shaped contract
against an HTTP catalogue adapter and an in-memory fake under two controlled
divergences:

- the fake returns zero instead of throwing `ProductNotFound` for an unknown
  SKU; and
- the HTTP adapter translates the provider's `404` into the same zero value.

The experiment asks whether every implementation preserves two named
caller-visible behaviors. It does not claim that a finite suite proves complete
substitutability.

## Run it

Docker is the only host dependency.

```sh
./run.sh all
```

Each mode can also run independently:

```sh
./run.sh clean
./run.sh fake-defect
./run.sh adapter-defect
```

The runner builds a digest-pinned image, starts a real PHP HTTP provider inside
each test container, executes every expected pass and failure, and returns
non-zero if the matrix is not reproduced. It retains the exact command,
expectation, exit code, duration, and console output under `results/`.

## Expected matrix

| Mode | HTTP contract | Fake contract | Known-SKU consumer controls |
| --- | --- | --- | --- |
| Clean | pass | pass | pass |
| Fake missing-SKU divergence | pass | fail | pass |
| HTTP missing-SKU divergence | fail | pass | pass |

The shared `ProductCatalogueContract` asserts the exact integer price for a
known SKU and the `ProductNotFound` exception for an unknown SKU. Its concrete
HTTP and fake subclasses provide only an implementation factory; they do not
override examples or expected values.

The consumer controls calculate the known product's subtotal through both
implementations. They remain green under both divergences because neither asks
for a missing SKU. That is the intended negative control: a happy path cannot
establish failure semantics it never executes.

## Contract boundary

| Behavior | Evidence owner |
| --- | --- |
| known SKU returns exact integer cents | shared contract |
| unknown SKU throws `ProductNotFound` | shared contract |
| provider `404` maps to `ProductNotFound` | HTTP contract execution |
| malformed payload is rejected | HTTP-adapter-specific test, outside this fixture |
| retries, authentication, and timeouts | separate integration or resilience tests |
| fake seed data is deterministic | fake setup |

The local provider is an actual HTTP process reached through Laravel's
production `Illuminate\Http\Client\Factory`. It serves one known product and a
`404` for every other SKU. The fixture does not claim to exercise DNS, TLS,
remote availability, authentication, or deployment configuration.

## Toolchain and measurement boundary

The Dockerfile pins PHP and Composer images by version and digest.
`composer.lock` pins Laravel 13.20.0, PHPUnit 12.5.31, and the complete
dependency graph. The runner records package versions and Composer's advisory
result with each run.

Durations include starting a disposable Docker container and local provider
for each command. They are reproduction metadata, not a benchmark of HTTP
adapters against in-memory fakes.
