Context
Deferred from PR #211 (#210 Part B) — the unit-level CAS-retry tests + multi-source E2E landed there, but the workflow matrix expansion needs an orchestration redesign and was scoped out.
Current state
`e2e.yml` runs one job: `dagger call test-e2e` → Python pytest fixture bakes a throwaway tag, runs the round-trip, auto-cleans the tag in the fixture finalizer. Multi-language client suites (JS/shell/Rust) have e2e blocks that gate on `MAT_VIS_E2E=1 + MAT_VIS_E2E_TAG`, but the workflow doesn't run them.
What's needed
Three-step workflow:
- bake-e2e-fixture job: dagger call to a NEW `bake_e2e_fixture` function that bakes 2 polyhaven + 2 ambientcg materials @ 1k, prints the tag to stdout. NO auto-cleanup.
- client-tests matrix job: `needs: bake-e2e-fixture`, `strategy.matrix.lang: [js, shell, rust]`. Each runs `dagger call test-client- --tag= --e2e`. Already-existing dagger functions just need an `--e2e` toggle that sets `MAT_VIS_E2E=1` + `MAT_VIS_E2E_TAG` instead of `MAT_VIS_TAG`.
- cleanup job: `needs: client-tests` (with `if: always()`) — dagger call to `cleanup_e2e_fixture` which deletes the branch + runs `audit-orphans`. Asserts orphans == 0 post-cleanup.
Why this matters (still)
The Python E2E gate already catches manifest emission gaps via its own bake-and-fetch. But client-side substrate slips (e.g. a JS client that builds a wrong URL, a shell client whose jq filter regresses against v3 catalogs) only surface when the client actually fetches real bytes. `test_client_*` runs against `v2026.04.0` (a non-existent legacy tag) which means those e2e blocks are skipped, not exercised.
Acceptance
Estimate
~80 LOC dagger + ~60 LOC YAML. Straightforward but interconnected.
Context
Deferred from PR #211 (#210 Part B) — the unit-level CAS-retry tests + multi-source E2E landed there, but the workflow matrix expansion needs an orchestration redesign and was scoped out.
Current state
`e2e.yml` runs one job: `dagger call test-e2e` → Python pytest fixture bakes a throwaway tag, runs the round-trip, auto-cleans the tag in the fixture finalizer. Multi-language client suites (JS/shell/Rust) have e2e blocks that gate on `MAT_VIS_E2E=1 + MAT_VIS_E2E_TAG`, but the workflow doesn't run them.
What's needed
Three-step workflow:
Why this matters (still)
The Python E2E gate already catches manifest emission gaps via its own bake-and-fetch. But client-side substrate slips (e.g. a JS client that builds a wrong URL, a shell client whose jq filter regresses against v3 catalogs) only surface when the client actually fetches real bytes. `test_client_*` runs against `v2026.04.0` (a non-existent legacy tag) which means those e2e blocks are skipped, not exercised.
Acceptance
Estimate
~80 LOC dagger + ~60 LOC YAML. Straightforward but interconnected.