Files
OpenMAIC/lib
wyuc 1a151c4a49 refactor(media): share one stored-bytes resolver across export paths (#1007 part 3a) (#1116)
* refactor(media): share one stored-bytes resolver across export paths

The classroom ZIP, PPTX, and video exports each re-implemented the same
pool-first byte resolution with a Dexie compatibility-row fallback and a
task-URL fallback. resolveStoredBytes in lib/media/resolve-stored-bytes.ts
now owns the chain, with the per-path differences (which levels run, fetch
validation strictness, state-machine gating) as explicit options. The
record-id to ref mapping moves alongside it as mediaRefFromRecordId.

Intended as a behavior-preserving extraction: same bytes, same fallback
order, same error handling. Cross-review found two places where this commit
did not achieve that, both repaired later on this branch -- it moved a
supplied row's `error` verdict past the pool await (restored by the snapshot
fix) and dereferenced `record.blob` unguarded (guarded by the option-surface
fix). See the PR description for the resulting divergence from main, which
is stated rather than claimed away.

Refs #1007

* fix(media): make the resolver's option surface mean what it documents

Cross-review round 1 surfaced the same class of defect from both reviewers:
options and docstrings that promise more than the code does.

- `taskUrlFallback` was inert without `resolutionGating`. The task lookup was
  derived from the gating flag alone, so the final level resolved with no task
  and always returned null. The lookup now runs whenever either concern needs
  it, while the gated levels still read it only when gating is on -- so no
  current caller's behavior changes, and the pre-refactor callers' own
  unconditional lookup is restored.
- `compatRowBytes` dereferenced `record.blob.size` unguarded, which broke the
  module's "never throws" contract and lost the guard the pre-refactor video
  path had (`blob && blob.size > 0`). A row that lost its blob now falls
  through to the next byte source.
- `loadCompatRow` (requires `stageId`) and `compatRowCdnFallback` (needs a row)
  now state their dependencies instead of silently doing nothing.

Six tests pin the behavior; each fix was mutation-checked (reverting the task
decoupling reds one, reverting the blob guard reds two).

* test(media): pin the resolver behavior the round-2 review found unpinned

Round 2 produced no behavior findings from either reviewer; every finding was
a test that would still pass with the behavior broken. Six cases close them.

Mutation-verified -- reverting the code each covers reddens it:

- Gating suppresses stale POOL and row bytes, not only the final level
  (removing either gate reds it).
- With gating off the compatibility row is still accepted while a task is
  generating. This is the discriminator for keeping `gate` separate from
  `task`: leaking the task into the gated levels reds it.
- A zero-length row blob falls through to the CDN source when enabled, and
  never reaches it when not (breaking `compatRowCdnFallback` in either
  direction reds one of the two).
- The effective ref comes from a supplied row's compound id, not the ref the
  caller passed (dropping the derivation reds it).
- The classroom ZIP still ships a non-OK pool response body -- the rebase
  conflict resolution that was correct but untested (tightening `requireOk`
  reds it).

Not mutation-verified, and marked as such in the file: "accepts pool bytes
with gating off" documents the pool level's unconditional branch but survives
every single-edit revert of the task/gate separation, because that level
branches on the option rather than on the gate. It is kept as documentation,
not as a sentinel.

* fix(media): snapshot a supplied row's error verdict before the pool await

Round 4 of cross-review found a divergence three rounds of level-by-level
comparison had missed. The pre-refactor video path computed
`usableRecord = record && !record.error` BEFORE awaiting the pool; the
extraction moved that test after the await. The supplied row is a live
object, so an `error` set (or cleared) while the pool lookup was pending
changed which bytes the export shipped.

The verdict is now taken where the caller took it, and -- the part the first
attempt at this fix got wrong -- it is not re-derived afterwards:
snapshotting only the reference still re-read `error` off the live object at
the later check. A row this function loads itself is still judged when it is
read, after the pool miss, because that read did not exist any earlier.

This was the last value read at a different point in the SEQUENCE than the
code it replaced. What remains is scheduling, not sequence: the row's `blob`
and `ossKey` are still read after the pool as they were on main, only one
microtask later. The PR description states that, and no writer in the repo
mutates a row in place across that window.

Two tests pin both directions (an error landing mid-lookup, and one
clearing); judging the supplied row after the await reds both.

* test(media): commit the frozen-base differential harness (F-1116-001)

The PR description claimed a differential comparison against the verbatim
pre-refactor implementations, but that comparison was run out of tree and
never committed, so no reviewer could re-run it. A claim that cannot be
reproduced is not evidence.

The three implementations are now reproduced verbatim as they stand at the
merge base -- still identical to main, since nothing between the two touched
them -- and driven side by side with the current helper under each call
site's real option set, over a declared matrix of ref shapes, task states,
pool outcomes, fetch outcomes, record shapes, and stage scoping. Every
combination compares the outcome (bytes, null, or the thrown type), blob
identity by reference, and the ordered lease / Dexie / fetch sequence
including arguments.

Absolute microtask offsets are not compared, for the reason the description
gives: the extraction moves later operations one or two ticks while leaving
the sequence, the arguments, and the results alone.

The blob-less PPTX row is the only permitted divergence, and the harness
pins where it may occur rather than merely tolerating it: every divergent
combination must be that row with a stage, and the count must equal the
combinations that actually reach the row -- all of them except the ones
where the frozen path returns pooled bytes first.

The harness bites: dropping the zero-byte rejection reds ZIP and video,
dropping the response.ok check reds PPTX and video, and dropping the
compound-id ref derivation reds video. Its scope is documented in the file --
option combinations no call site uses, and state that mutates mid-await, are
covered by the targeted tests instead.

* test(media): extend equivalence harness for colon refs and PPTX divergence (F-1116-001)
2026-08-18 12:10:52 +08:00
..
2026-08-09 07:34:12 -04:00