test(producer): reproducer fixture for both-escape-hatches-fail case

Field signal ts=1784039841 (win32/x64, CLI 0.7.57): shifted-DOM-image-
layer bug at frame 120 reproduces with BOTH PRODUCER_FORCE_SCREENSHOT=true
AND HF_DE_PARALLEL_ROUTER=false set. First case where both known escape
hatches fail simultaneously. Standalone 1920x1080 GSAP paused timeline,
absolute PNG layers, 4 independent scenes.

Ships a skipped regression fixture to codify the shape. NOT a fix — no
root cause identified. The composition is preserved so a future
diagnostic pass has a real, checked-in repro and a proposed fix can be
validated against the same shape the field reported.

Skip mechanism (belt-and-suspenders):
  1. meta.json tagged `field-signal-reproducer` and `known-broken`;
     producer/package.json test:regression* scripts add the tag to
     --exclude-tags alongside the existing `transparency` skip.
  2. Not registered in any .github/workflows/regression.yml shard's
     args, so the CI regression sweep won't pick it up either.

Un-skip when a fix lands: drop the tags from meta.json AND add the
fixture id to a shard's args in the workflow. See src/README.md for
the field-signal envelope and diagnostic starting points.

Stack: PR #8 of 9 (base via/gpu-parity-gate).

Signed-off-by: Via
This commit is contained in:
Via
2026-07-15 23:35:58 +00:00
parent 97e094621f
commit e5c4e1970c
4 changed files with 328 additions and 4 deletions
+4 -4
View File
@@ -51,15 +51,15 @@
"test:integration": "bun run test:classification && node scripts/run-test-lane.mjs integration",
"test:integration:bun": "node scripts/run-test-lane.mjs integration bun",
"test:integration:vitest": "node scripts/run-test-lane.mjs integration vitest",
"test:regression": "tsx src/regression-harness.ts --exclude-tags transparency",
"test:regression:update": "tsx src/regression-harness.ts --update --exclude-tags transparency",
"test:distributed": "tsx src/regression-harness.ts --exclude-tags transparency --mode=distributed-simulated",
"test:regression": "tsx src/regression-harness.ts --exclude-tags transparency,field-signal-reproducer",
"test:regression:update": "tsx src/regression-harness.ts --update --exclude-tags transparency,field-signal-reproducer",
"test:distributed": "tsx src/regression-harness.ts --exclude-tags transparency,field-signal-reproducer --mode=distributed-simulated",
"test:transparency": "tsx src/transparency-test.ts",
"docker:build:test": "docker build -f ../../Dockerfile.test -t hyperframes-producer:test ../..",
"docker:test": "docker run --rm --security-opt seccomp=unconfined --shm-size=2g -v ./tests:/app/packages/producer/tests hyperframes-producer:test",
"docker:test:update": "docker run --rm --security-opt seccomp=unconfined --shm-size=2g -v ./tests:/app/packages/producer/tests hyperframes-producer:test --update",
"docker:test:distributed": "docker run --rm --security-opt seccomp=unconfined --shm-size=2g -v ./tests:/app/packages/producer/tests hyperframes-producer:test --mode=distributed-simulated",
"test:lambda-local": "tsx src/regression-harness.ts --exclude-tags transparency --mode=lambda-local",
"test:lambda-local": "tsx src/regression-harness.ts --exclude-tags transparency,field-signal-reproducer --mode=lambda-local",
"docker:test:lambda-local": "docker run --rm --security-opt seccomp=unconfined --shm-size=2g -v ./tests:/app/packages/producer/tests hyperframes-producer:test --mode=lambda-local",
"prepublishOnly": "echo skip"
},
@@ -0,0 +1,13 @@
{
"name": "escape-hatch-fatal-fallback",
"description": "Field-signal reproducer (ts=1784039841). Shifted-DOM-image-layer bug at frame 120 that reproduces with BOTH known escape hatches applied (PRODUCER_FORCE_SCREENSHOT=true AND HF_DE_PARALLEL_ROUTER=false — the conservative defaults). Standalone 1920x1080 GSAP paused timeline, absolute PNG layers, 4 independent scenes. NO ROOT CAUSE IDENTIFIED — this fixture codifies the failing composition shape so a future fix can be validated against a real repro. Tagged 'field-signal-reproducer' and 'known-broken' so the CI regression sweep and local `bun run test` both skip it via --exclude-tags; un-skip (drop the tag OR add the fixture id to a shard's args in .github/workflows/regression.yml) when a fix lands. See src/README.md for the field-signal envelope and the diagnostic starting points.",
"tags": ["regression", "field-signal-reproducer", "known-broken", "render-compat"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
@@ -0,0 +1,120 @@
# escape-hatch-fatal-fallback — field-signal reproducer
> **This is a REPRODUCER, not a FIX.** No root cause has been identified. The
> fixture exists so a future diagnostic pass has a real, checked-in composition
> that reliably surfaces the shift, and so a proposed fix can be validated
> against the same shape the field reported.
## Field signal envelope
- `ts=1784039841`
- Platform: `win32/x64`
- CLI version: `0.7.57`
- Reported in `#hyperframes-cli-feedback`
## Failure mode
At **frame 120** of a 30fps render (i.e. t=4.0s), a DOM `<img>` layer positioned
absolutely inside the composition shifts by a small but visually obvious offset
from its baseline position. The shift is not an animation frame — it is a
compositor artifact that appears in the encoded output and is absent from the
authored timeline.
## Why this fixture matters — both escape hatches fail
The two known escape hatches for shifted-layer / compositor-race classes of bug
are:
- **`PRODUCER_FORCE_SCREENSHOT=true`** — forces the "screenshot" capture path
in `packages/producer/src/services/renderOrchestrator.ts` and
`packages/engine/src/config.ts`. This is also the default; setting it
explicitly is idempotent with the default state, but the field report
set it explicitly to rule out env-inference bugs.
- **`HF_DE_PARALLEL_ROUTER=false`** — forces the distributed-encoder parallel
router OFF in `packages/producer/src/services/renderOrchestrator.ts` (default
is off; the router is opt-in via `HF_DE_PARALLEL_ROUTER=true`).
Prior escape-hatch cases (e.g. the RAM-pressure `Runtime.callFunctionOn`
cluster tracked around #1087 / #2504) had at least one working fallback: either
the screenshot path or the single-worker path would produce a clean render even
when the other was regressing.
**This case has none.** Applying both escape hatches leaves the shift intact.
That's the anomaly worth codifying — every prior triage playbook for this
class of bug assumed at least one lever worked.
## Composition shape (verbatim from field envelope)
- Standalone 1920x1080 render (no host composition; no picture-in-picture)
- GSAP `paused: true` timeline, driven from outside via `tl.progress()` /
`tl.seek()` (matches `parallel-capture-regression` and every other
GSAP-driven HF regression fixture)
- **Absolute PNG layers** — `<img>` tags positioned via `position: absolute`
with explicit `top` / `left` / `width` / `height`
- **4 independent scenes** — no cross-fades that overlap the shift frame;
the middle 1.1s of scene 2 (which contains frame 120) is a clean opacity=1
window with no compositional cross-traffic
`src/index.html` implements exactly this shape. PNG bytes are inlined as tiny
data URIs (1x1 solid colors that the layout scales to layer size) — this
preserves the layout shape without shipping binary assets. If diagnostic work
reveals the shift is sensitive to real image decode paths (e.g. sRGB gamma,
non-square intrinsic dimensions, animated PNG chunks), swap the inline URIs
for byte-identical copies of the field asset and re-run.
## How this fixture is skipped
The fixture is tagged `field-signal-reproducer` and `known-broken` in
`meta.json`. Two mechanisms combine to keep CI green:
1. **Local `pnpm test` / `bun run test`** — the producer package's `test`
script (`packages/producer/package.json`) exclude the
`field-signal-reproducer` tag via `--exclude-tags`, alongside the
pre-existing `transparency` exclusion.
2. **CI regression sweep**`.github/workflows/regression.yml` runs shards
with explicit fixture-name arg lists. This fixture's name is not in any
shard, so it won't be picked up.
Both belt-and-suspenders are intentional. If a future dev drops the tag
without updating the workflow (or vice versa), the fixture stays skipped.
## When a fix lands
1. Verify the fix eliminates the shift by running the fixture manually:
```
PRODUCER_FORCE_SCREENSHOT=true HF_DE_PARALLEL_ROUTER=false \
bun run --cwd packages/producer test escape-hatch-fatal-fallback
```
The `--exclude-tags` filter is bypassed when an explicit fixture name is
passed — the tag exclusion only fires when the harness is enumerating.
2. Drop the `field-signal-reproducer` and `known-broken` tags from `meta.json`
(keep `regression` and `render-compat`).
3. Add `escape-hatch-fatal-fallback` to the currently-lightest shard's `args`
in `.github/workflows/regression.yml` (see the LPT-heuristic comment there
for balancing guidance).
4. If baselines don't exist yet, run `bun run --cwd packages/producer
test:update escape-hatch-fatal-fallback` to freeze them, then commit the
`output/` directory contents that the harness writes.
## Diagnostic starting points
For whoever picks this up — a few threads worth pulling before treating the
composition shape as the root cause:
- **Does the shift move to a different frame if the fixture duration
changes?** If frame 120 tracks with 4s wall-clock, it's a timing-side
effect. If it tracks with the third-scene boundary regardless of duration,
it's a scene-transition effect.
- **Does the shift survive a single-worker in-process render?** The
in-process path lives in `packages/producer/src/regression-harness.ts` and
bypasses both the screenshot toggle and the DE parallel router. If the
shift disappears in-process, the fatal-fallback surface is inside the
Docker/lambda capture harness, not the composition renderer.
- **Does the shift survive on Linux/darwin as well as win32/x64?** The field
report is win32-only. Cross-platform reproduction would rule out a
win32-specific input pipeline (Chromium's Windows compositor thread has a
history of shipping DOM shifts under memory pressure).
- **Compare against the `parallel-capture-regression` fixture** — same
GSAP-paused shape, no reported shift. What's the delta? Likely candidates:
absolute-positioned `<img>` decode timing, per-scene opacity gating, or the
layer's y/x tween during the shift window.
@@ -0,0 +1,191 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
/*
* Field-signal reproducer (ts=1784039841).
*
* Composition shape:
* - 1920x1080 root
* - GSAP paused timeline (window.__timelines[...] pattern)
* - 4 independent scenes (data-start = 0/1.5/3/4.5, each 1.5s)
* - Each scene contains one <img> with position:absolute (the
* "absolute PNG layer" from the field signal)
* - Frame 120 @ 30fps = 4s into the timeline → scene 3 boundary.
* Field report says the shift lands at exactly this frame.
*
* PNG sources are inline data URIs — 1x1 solid-color pixels the
* browser scales to layer size. This preserves the layout shape
* (absolute-positioned <img> with intrinsic dimensions from a real
* decoded PNG) without shipping binary assets. If the shift bug turns
* out to be sensitive to real image decode paths, the fix reviewer
* should swap these for byte-for-byte replicas of the field asset
* and re-run; the field envelope did not preserve the original PNGs.
*/
body,
html {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #101418;
font-family: system-ui, -apple-system, sans-serif;
color: #e6edf3;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
.scene {
position: absolute;
inset: 0;
opacity: 0;
}
.scene-label {
position: absolute;
top: 48px;
left: 64px;
font-size: 40px;
font-weight: 700;
letter-spacing: 0.04em;
}
.frame-marker {
position: absolute;
right: 64px;
top: 48px;
font-size: 32px;
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
/*
* "Absolute PNG layer" — an <img> positioned via top/left absolute
* coordinates. Field signal specifies this shape explicitly. Each
* scene has one primary layer; total composition therefore has 4
* absolute PNG layers, one per scene.
*/
.layer {
position: absolute;
top: 300px;
left: 660px;
width: 600px;
height: 480px;
}
/*
* Scene 3 anchors at data-start=3s → frame 90 (30fps). Its layer
* lifetime crosses frame 120 (the reported shift frame). Keep the
* bounding box conspicuous so a diff at frame 120 lights up.
*/
.scene[data-scene="3"] .layer {
top: 240px;
left: 600px;
width: 720px;
height: 540px;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="escape-hatch-fatal-fallback"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="6"
>
<div class="scene clip" data-scene="0" data-start="0" data-duration="1.5">
<div class="scene-label">Scene 0 · 0.0s 1.5s</div>
<div class="frame-marker">frames 044</div>
<img
class="layer"
alt=""
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
style="background: #4f46e5"
/>
</div>
<div class="scene clip" data-scene="1" data-start="1.5" data-duration="1.5">
<div class="scene-label">Scene 1 · 1.5s 3.0s</div>
<div class="frame-marker">frames 4589</div>
<img
class="layer"
alt=""
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGAe5wu7AAAAABJRU5ErkJggg=="
style="background: #0ea5e9"
/>
</div>
<div class="scene clip" data-scene="2" data-start="3" data-duration="1.5">
<div class="scene-label">Scene 2 · 3.0s 4.5s</div>
<div class="frame-marker">frames 90134 (shift @ 120)</div>
<img
class="layer"
alt=""
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYP5fDwADgQGAxSSFVQAAAABJRU5ErkJggg=="
style="background: #22c55e"
/>
</div>
<div class="scene clip" data-scene="3" data-start="4.5" data-duration="1.5">
<div class="scene-label">Scene 3 · 4.5s 6.0s</div>
<div class="frame-marker">frames 135179</div>
<img
class="layer"
alt=""
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgADkgGA+A6NvgAAAABJRU5ErkJggg=="
style="background: #f97316"
/>
</div>
</div>
<script>
// Paused-timeline pattern — matches parallel-capture-regression and every
// other GSAP-driven HF regression fixture. The producer calls tl.progress()
// or tl.seek() from outside; the timeline never advances on its own.
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var duration = 6;
// Each scene fades in as its window opens and out as the next arrives.
// Fade windows are 200ms; the middle 1.1s of each scene sits at opacity=1,
// so the "still" state at frame 120 (scene 2, ~0.5s into its 1.5s window)
// is unambiguous — no cross-fade interference to muddy a shift diff.
var scenes = document.querySelectorAll(".scene");
scenes.forEach(function (scene) {
var start = parseFloat(scene.getAttribute("data-start") || "0");
var dur = parseFloat(scene.getAttribute("data-duration") || "0");
tl.fromTo(
scene,
{ opacity: 0 },
{ opacity: 1, duration: 0.2, ease: "power1.out" },
start,
);
tl.to(scene, { opacity: 0, duration: 0.2, ease: "power1.in" }, start + dur - 0.2);
});
// A subtle drift on the shift-frame scene's layer. If a fix eliminates
// the underlying compositor shift, the on-timeline motion here should
// still match baseline pixel-for-pixel — the "shift" being reproduced
// is an *unwanted* offset on top of this deliberate motion.
tl.to(
'.scene[data-scene="2"] .layer',
{ x: 24, y: 12, duration: 1.5, ease: "none" },
3,
);
window.__timelines["escape-hatch-fatal-fallback"] = tl;
</script>
</body>
</html>