Skip to content

Finish the Expo SDK migration: SDK 52→57, RN 0.76.5→0.86.2, expo-av → expo-audio + expo-video - #2056

Open
rbuergi wants to merge 5 commits into
mainfrom
feat/expo-sdk57-rn082
Open

Finish the Expo SDK migration: SDK 52→57, RN 0.76.5→0.86.2, expo-av → expo-audio + expo-video#2056
rbuergi wants to merge 5 commits into
mainfrom
feat/expo-sdk57-rn082

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Finishes the migration #1584 opened: Expo SDK 52 → 57, react-native 0.76.5 → 0.86.2, react 18.3.1 → 19.2.8, and expo-avexpo-audio + expo-video.

One correction to the issue title up front: SDK 57 does not pair with RN ~0.82. expo@57.0.15's own bundledNativeModules.json names react-native: 0.86.2 (which is what clients/react-native/package.json's existing note already said — "expo@57 + react-native@0.86"). Every version in this PR is taken from that file rather than chosen, and expo-av is not in it at all.


1. The expo-av call-site split — enumerated before anything was changed

expo-av is a package split, not a rename, so every site was sorted first. There are exactly two:

# Site Became What it does
1 src/speech/expoRecorder.ts expo-audio microphone capture for the speech → /api/speech/transcribe pipeline
2 src/rnContainers.tsx expo-video the Video leaf of the RN control pack

So: 1 audio, 1 video. Plus five supporting references that are bookkeeping, not API: test/expo-av.mock.tsx (replaced), the vitest alias, the app.json config-plugin entry, the package.json dependency, and a comment + class name in App.tsx/README.md.

What consumes this client (checked, per the brief)

The speech lane is real and it is the thing at risk. ExpoAudioRecorderAudioInput → multipart POST to POST /api/speech/transcribe (memex/Memex.Portal.Shared/Api/SpeechEndpoints.cs, and the LocalMesh twin in memex/Memex.LocalMesh/Program.cs) → the Whisper container. What that endpoint consumes is the FORMAT, not the API, so the format is preserved exactly: 16 kHz mono, WAV/LINEARPCM on iOS, AAC/.m4a on Android, same contentType/fileName. The Recorder interface is untouched, so PushToTalkController and its 16 tests never see the vendor change.

VideoControl.Poster is likewise a real server-side field (src/MeshWeaver.Layout/VideoControl.cs, rendered by Blazor as <video poster>) — see below.


2. What changed BEHAVIOUR, not just API surface

Everything else is a mechanical rename. These five are not:

a. Video lost its poster prop — and it is reimplemented rather than dropped. expo-video has no posterSource/usePoster; VideoViewProps carries no poster of any kind, and VideoSource.metadata.artwork is the lock-screen image, not an in-view one. Since VideoControl.Poster is a documented field Blazor honours, it is reproduced as an overlay Image inside a pointerEvents="none" View (so the native play button still takes the tap), cleared on the first playingChange. Nuance: expo-av swapped the poster out when the video loaded; this clears it when playback starts — which is what <video poster> does. Three unit tests pin it.

b. Recording duration is now read before stop(), not returned by it. expo-av's stopAndUnloadAsync() resolved a status carrying durationMillis; expo-audio's stop() resolves void, and the web recorder drops its MediaRecorder inside stop(), after which getStatus() can no longer measure. So the duration is sampled from getStatus() immediately before stopping. It is therefore a few ms short of the old value. (durationMs is produced but never consumed anywhere in the tree.)

c. Expo web's recording label was wrong, and is corrected. Both this file and its expo-av predecessor branched on Platform.OS === "ios" alone, so Expo web — which records audio/webm through MediaRecorder, and always did — reached the transcribe endpoint labelled .m4a / audio/mp4. The port made it visible because expo-audio's RecordingOptions spells the web mimeType out next to the extension. A single CONTAINER now keeps extension/contentType/fileName together across three cases. (Deliberately NOT fixed, and recorded in the file: web capture is still not end-to-end either way — the recorder yields a blob: URL and transcription.ts posts a uri through React Native's native-only {uri, name, type} FormData extension. That gap predates this port.)

d. bitRate moved from per-platform to top level. expo-audio's RecordingOptions hoists extension/sampleRate/numberOfChannels/bitRate out of the platform blocks, and bitRate has no per-platform slot any more. expo-av carried two values (256 kbps iOS / 64 kbps Android), so they are now selected by Platform. Same values, same files.

e. app.json: enableBackgroundPlayback: false is passed explicitly. expo-audio's config plugin defaults it to true, which would add UIBackgroundModes: [audio] to Info.plist plus FOREGROUND_SERVICE_MEDIA_PLAYBACK and a media service on Android — to an app that only records. expo-av declared none of that; an unused background-audio entitlement is an App Store review flag, so the default is overridden to preserve the previous native surface.

f. 🚨 A blank page on the web export — an upstream ordering bug in expo-video, found by RUNNING the export. After the port, npm run web:export succeeded and all 5 Playwright tests failed with "element(s) not found": the page was the noscript text, with one error before React ever mounted —

TypeError: Cannot read properties of undefined (reading 'SharedObject')

expo-video@57's web build defines class VideoPlayerWeb extends globalThis.expo.SharedObject at module-evaluation time (build/VideoPlayer.web.js), and nothing in its own import graph reaches expo-modules-core, whose src/index.ts is what runs installExpoGlobalPolyfill():

expo-video/index → ./VideoModule → NativeVideoModule.web         (no imports)
                 → ./VideoView   → VideoView.web → VideoPlayer.web   ← throws here

Nor does this app's entry: expo/AppEntry.js → registerRootComponent → Expo.fx.web → winter / async-require / rsc, none of which touch expo-modules-core. Router-based Expo apps get the global for free from their entry, which is why upstream ships this. expo-audio is unaffected — its index.js re-exports from expo on the first line.

A side-effect import "expo" above the expo-video import installs it. That in turn pulled the whole Expo runtime into vitest, where expo/src/async-require/setup.ts reads __DEV__ (a Metro-transform define) and killed 10 of 16 suites — so the test tree aliases expo to a stub that installs the same global with a usable SharedObject base, keeping the reason for the import visible instead of aliasing it to nothing.

This is exactly the failure that typecheck + build cannot see. It typechecks perfectly and bundles without a warning.


3. Holdbacks retired (the migration's other half)

package.json and .github/dependabot.yml both carried holdbacks whose stated reason was the SDK 52 pin, with an explicit instruction to lift them as part of this migration. Each was re-measured on SDK 57, not assumed:

  • typescript → 7.0.2 — RETIRED. The blocker was @expo/cli SDK 52 reading tsconfig through the classic TS compiler API that TS 7 deleted. SDK 57's CLI does not: expo export --platform web runs clean on a cold Metro cache (503 modules) under 7.0.2, and Playwright is 5/5 on the resulting bundle. Two tsconfig changes belong to TS 7 itself: baseUrl is removed (TS5102 — every paths entry here is already relative, which is what baseUrl: "." meant), and @types is no longer auto-included, so types: ["node"] + a declared @types/node — the same fix, for the same TS2591, on the same kind of file, that clients/react already carries. This puts the RN package on the same TypeScript as the other six client workspaces.
  • react / @types/react majors — kept, reason rewritten. RN 0.76.5's hard react ^18.2.0 peer is gone (0.86.2 peers react ^19.2.3), but react's major here is still whatever react-native peers, so the coupling outlives the version.
  • react-native / react-native-web / expo* / @expo* majors — unchanged. That coupling is the whole reason this directory has its own dependabot config.
  • The tsconfig React-identity dedupe survives untouched, exactly as Finish the Expo SDK migration for clients/react-native: RN 0.76.5 → ~0.82 and expo-av → expo-audio/expo-video (currently held back at SDK 52) #1584's own comment predicted: both packages are on React 19 now and it is still required, because the problem is two node_modules trees sharing source, not a version skew.

CI Node 20 → 22 for both RN jobs: react-native 0.86 and metro declare engines.node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0. Node 20 clears that floor only on its final patches and went EOL 2026-04-30, so node-version: 20 would leave the jobs depending on setup-node continuing to resolve a 20.19.4+ patch of an unsupported line. The react job has been on 22 all along.


4. Verified here

After a clean rm -rf node_modules && npm ci (so the lockfile is proven, not just the working tree):

Gate Result
npm ci ✅ resolves — no --legacy-peer-deps, no overrides entry, no forced peer
npx tsc --noEmit (TS 7.0.2) ✅ 0 errors
npx vitest run 16 files / 161 tests
expo export --platform web (cold cache) ✅ 503 modules, 1.2 MB bundle
npm run e2e (Playwright, real Chromium) 5/5
clients/react: typecheck / test / build ✅ 0 / 35 files, 261 tests / 0 — it reads the workflow file this PR edits (ciTrigger.test.ts)

…and on CI: the Clients workflow ran green on all 8 jobs, with both RN jobs verified from the job LOG rather than the colour — RN connector shows 16 passed (16) / 161 passed (161) on Node 22, RN web (Playwright) shows Web Bundled … (505 modules) and 5 passed.

Review findings, both fixed (0f96653)

Copilot raised two, both correct:

  • the Expo-web container mismatch — §2c above;
  • the Video poster's started flag was sticky: useVideoPlayer memoizes on the source, so a control whose Src changed in place got a new player while started stayed true, and the new video's poster never appeared. Reset with the player now. The regression test was verified non-vacuous — remove setStarted(false) and it fails, restore it and it passes.

The Clients workflow will run on this diff — its path filter lists both clients/** and .github/workflows/clients.yml, and this PR touches both. Not a skip.


5. 🚨 What I could NOT verify

Stated plainly, because an RN upgrade that compiles in CI and breaks on device is worse than one that is honestly reported as unverified.

No native build was performed at all. No expo prebuild, no pod install, no Xcode or Gradle build, no simulator, no device. ios/ and android/ are gitignored, so nothing stale is committed — but nothing has been regenerated or built either. Concretely, this means:

  1. Microphone capture on iOS/Android is unverified. expo-audio is a native module. The recording options map 1:1 onto the previous ones and the format contract is preserved by construction, but no recording was made and no audio was posted to /api/speech/transcribe. The first native build is where a wrong outputFormat string or a permission-plugin change would show up.

  2. Video playback on device is unverified. The poster overlay is pinned by unit tests against a mock player; a real VideoPlayer emitting playingChange has never driven it. contentFit="contain" vs ResizeMode.CONTAIN is likewise a types-level equivalence, not an observed one.

  3. The New Architecture is unexercised. app.json already carried newArchEnabled: true, so this is not a flag flip — but RN 0.86 makes it the only mode and the native side has never been built under it here. Six RN minors (0.76 → 0.86) also regenerate the native template; react-native-render-html@6 and react-native-fetch-api in particular have not been run against 0.86 on a device.

  4. Only the web target was driven end to end. react-native-web is what the Playwright job renders. Every native claim above rests on the type checker and on upstream's own version pairing.

  5. npx expo install --check advisories, unresolved on purpose: it expects react/react-dom 19.2.3 (we ship 19.2.8, which satisfies RN 0.86.2's ^19.2.3 peer and matches clients/react, so the two source-shared packages stay on one version), and typescript ~6.0.3 (TS 6 is beta on npm; latest is 7.0.2, and all six other client workspaces are on ^7.0.2).

  6. MeshWeaver.AI.Test is red on this branch, and it is a pre-existing trunk flake — evidenced, not waved through. Two runs, two different tests in that one suite, each an observable that never emitted:

    run failing test shape
    1st (b54cd0bf) ThreadTokenUsageTest.CompletedRound_ProviderOmitsTotal_DerivesTotalFromInPlusOut ObservableAssertionException: … within 20s … emitted nothing at all
    2nd (0f966534) ProbeHubCostTest.ValidateContentAgainstSchema_DoesNotBuildTheNodeControlPlane same

    The second is the same test name that fails on main — main runs 32564051012 and 32559747093 both die on ProbeHubCostTest.ValidateContentAgainstSchema_DoesNotBuildTheNodeControlPlane, and 32562019976 on its sibling GetContentSchema_…. That is 3 of main's last 8 runs, all in MeshWeaver.AI.Test.

    And this PR's diff contains zero C# files — 16 files, all of them clients/react-native/** plus .github/dependabot.yml and .github/workflows/clients.yml — so it has no path to MeshWeaver.AI.Test at all. Recorded here so the red is not mistaken for fallout from this change; it needs its own issue, not a fix in this PR.

Recommended before this is trusted on a device: one npx expo prebuild --clean + expo run:ios / expo run:android, then press-and-hold the chat mic once and play one Video leaf with a poster.

Closes #1584.

🤖 Generated with Claude Code

rbuergi and others added 4 commits August 22, 2026 11:29
Dependency half of #1584. Every version is the pairing expo@57's own
bundledNativeModules.json names, not a guess:

  react-native        0.76.5  -> 0.86.2   (peers react ^19.2.3, which the tree already has)
  react / react-dom   18.3.1  -> 19.2.8   (matches clients/react)
  react-native-web    0.19.13 -> 0.21.2
  react-native-svg    15.8.0  -> 15.15.4
  expo                52      -> 57.0.15  (+ every expo-* to its 57.x line)
  expo-av             15.1.7  -> REMOVED  (not in SDK 57 at all)
  expo-audio / expo-video     -> NEW      (the two halves expo-av split into)

typescript deliberately stays 5.x: @expo/cli still reads tsconfig through the
classic TS compiler API that TS 7 deleted, so `expo export` (the RN web
Playwright job) dies without it. The call-site port follows in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The substantive half of #1584. expo-av is not a rename — it is a SPLIT into two
packages with different APIs, so each call site had to be sorted:

  src/speech/expoRecorder.ts  -> expo-audio   (microphone capture, 1 site)
  src/rnContainers.tsx        -> expo-video   (the Video leaf, 1 site)

Capture (expo-audio). The FORMAT contract is what /api/speech/transcribe and the
Whisper container consume, so it is preserved exactly — 16 kHz mono, WAV/LINEARPCM
on iOS, AAC/.m4a on Android, same contentType/fileName. What moved:

  - Audio.Recording.createAsync() -> construct + prepareToRecordAsync() + record()
  - stopAndUnloadAsync() returned the duration; stop() resolves void, so the
    duration is read from getStatus() BEFORE stopping (the web recorder drops its
    MediaRecorder inside stop(), after which it can no longer measure).
  - recording.getURI() -> recorder.uri
  - allowsRecordingIOS/playsInSilentModeIOS -> allowsRecording/playsInSilentMode
  - RecordingOptions moved extension/sampleRate/channels/bitRate from PER-PLATFORM
    to top level. bitRate has no per-platform slot at all now, so expo-av's two
    values (256k iOS / 64k Android) are selected by Platform.
  - expo-audio ships no imperative recorder constructor under one name on every
    platform (native: AudioModule.AudioRecorder; web: AudioModule.AudioRecorderWeb,
    and the types describe only the native shape). Both implement the same
    interface, so the ctor is resolved at runtime behind one documented cast.

  Renamed ExpoAvRecorder -> ExpoAudioRecorder; the Recorder seam is unchanged, so
  PushToTalkController and its tests are untouched.

Video (expo-video). Prop renames are mechanical (useNativeControls ->
nativeControls, resizeMode={ResizeMode.CONTAIN} -> contentFit="contain") and the
source moved off the view onto a player (useVideoPlayer). One prop has NO
successor: expo-video has no posterSource/usePoster and VideoViewProps carries no
poster of any kind. VideoControl.Poster is a real field that Blazor honours as
<video poster>, so rather than drop it silently it is reproduced as an overlay
that clears on the first playingChange, inside a pointerEvents="none" View so the
native play button still takes the tap. Three tests pin it.

app.json: the expo-av plugin becomes expo-audio with the same microphonePermission,
plus enableBackgroundPlayback:false — that option DEFAULTS TO TRUE and would add
UIBackgroundModes:[audio] and FOREGROUND_SERVICE_MEDIA_PLAYBACK to a build that
only records.

typecheck 0 errors; vitest 16 files / 161 tests green; npm ci replays the lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by RUNNING the web export, not by typechecking it: with SDK 57 the exported
app was a blank page (the noscript text) and all 5 Playwright tests failed on
"element(s) not found", with one page error before React ever mounted —

  TypeError: Cannot read properties of undefined (reading 'SharedObject')

expo-video@57's web build defines `class VideoPlayerWeb extends
globalThis.expo.SharedObject` at MODULE-EVALUATION time (build/VideoPlayer.web.js),
and nothing in its own import graph reaches expo-modules-core, whose src/index.ts
is what runs installExpoGlobalPolyfill() and creates globalThis.expo on web:

  expo-video/index -> ./VideoModule -> NativeVideoModule.web   (no imports)
                   -> ./VideoView   -> VideoView.web -> VideoPlayer.web  <- throws

Nor does this app's entry: expo/AppEntry.js -> registerRootComponent ->
Expo.fx.web -> winter / async-require / rsc, none of which touch
expo-modules-core. Router-based Expo apps get the global for free from their
entry, which is why upstream ships this. expo-audio is unaffected — its index.js
re-exports from 'expo' on the first line.

A side-effect `import "expo"` above the expo-video import installs it. That pulls
the whole Expo runtime into vitest, where expo/src/async-require/setup.ts reads
__DEV__ (a Metro-transform define) and killed 10 of 16 suites, so the test tree
aliases `expo` to a stub that installs the same global with a usable SharedObject
base — the reason the import exists stays visible instead of being aliased to
nothing.

typecheck 0; vitest 16 files / 161 tests; expo export + Playwright 5/5 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last piece of #1584: clients/react-native/package.json and
.github/dependabot.yml both carried holdbacks whose stated reason was the SDK 52
pin, with an explicit instruction to lift them AS PART of this migration. Each
was re-measured on SDK 57 rather than assumed:

  typescript -> 7.0.2 (RETIRED). The blocker was @expo/cli SDK 52 reading tsconfig
  through the classic TS compiler API that TS 7 deleted. SDK 57's CLI does not:
  `expo export --platform web` runs clean on a COLD Metro cache (503 modules) under
  7.0.2, and the Playwright e2e is 5/5 on the resulting bundle. Two tsconfig changes
  belong to TS 7 itself, not to Expo: `baseUrl` is removed (TS5102 — every `paths`
  entry here is already relative, which is what baseUrl:"." meant), and @types is no
  longer auto-included, so `types:["node"]` + a declared @types/node — the same fix,
  for the same TS2591 on the same kind of file, that clients/react already carries.

  react/@types/react majors: kept, but the REASON is rewritten. The 0.76.5-era
  hard `react ^18.2.0` peer is gone; what remains is that react's major here is
  whatever react-native peers, so it still arrives with the SDK jump or not at all.

  react-native / react-native-web / expo* / @expo* majors: unchanged. That coupling
  is the whole reason this directory has its own dependabot config.

CI Node 20 -> 22 for both RN jobs. react-native 0.86 and metro declare
engines.node ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0; Node 20 clears that
floor only on its final patches and went EOL 2026-04-30, so `node-version: 20`
would leave the jobs depending on setup-node continuing to resolve a 20.19.4+
patch of an unsupported line. The react job has been on 22 all along.

Verified after a clean `npm ci`: tsc 0, vitest 16 files / 161 tests, cold
`expo export --platform web`, Playwright 5/5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 22, 2026 09:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Completes the Expo SDK migration for clients/react-native, aligning the package with Expo SDK 57’s upstream version pairing (RN 0.86.2 / React 19.x) and replacing deprecated expo-av usage with expo-audio (recording) and expo-video (video playback).

Changes:

  • Upgraded Expo/RN/React/TypeScript to the SDK 57 + RN 0.86.2 + React 19 toolchain and updated CI Node version for RN jobs.
  • Ported expo-av call sites to expo-audio (speech recorder) and expo-video (Video control), adding test scaffolding/mocks as needed.
  • Adjusted RN web/test environment wiring (Vitest aliases + new Expo stub) to accommodate expo-video’s web initialization ordering.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
clients/react-native/vitest.config.ts Swaps module aliases from expo-av to expo-video and adds an expo stub for tests.
clients/react-native/tsconfig.json Updates TS 7-related compiler options and keeps path-based type identity dedupe.
clients/react-native/test/react-native.mock.tsx Extends RN mock StyleSheet to include absolute-fill helpers.
clients/react-native/test/expo.mock.ts Adds an expo stub to install globalThis.expo for headless tests.
clients/react-native/test/expo-video.mock.tsx Adds a minimal expo-video mock (player + view) for unit tests.
clients/react-native/test/expo-av.mock.tsx Removes the old expo-av mock.
clients/react-native/src/speech/expoRecorder.ts Ports the recorder implementation from expo-av to expo-audio.
clients/react-native/src/rnContainers.tsx Ports the Video control from expo-av to expo-video, including poster overlay behavior.
clients/react-native/src/rnContainers.test.tsx Updates/extends unit tests to validate the new expo-video mapping and poster overlay behavior.
clients/react-native/README.md Updates documentation to reference ExpoAudioRecorder / expo-audio.
clients/react-native/package.json Updates dependencies to Expo SDK 57 stack; replaces expo-av with expo-audio + expo-video.
clients/react-native/App.tsx Renames recorder usage to ExpoAudioRecorder and updates related comments.
clients/react-native/app.json Switches config plugin from expo-av to expo-audio and explicitly disables background playback.
.github/workflows/clients.yml Bumps RN job Node version to 22 for SDK 57 / RN 0.86 engine requirements.
.github/dependabot.yml Updates RN/Expo holdback rationale now that SDK 57 migration is completed.
Suppressed comments (1)

clients/react-native/src/speech/expoRecorder.ts:150

  • On web, stop() returns contentType: "audio/mp4" / fileName: "audio.m4a" even though the configured web recorder uses mimeType: "audio/webm". This makes the returned AudioInput metadata inconsistent with the actual file format.
    return {
      uri,
      contentType: ios ? "audio/wav" : "audio/mp4",
      fileName: ios ? "audio.wav" : "audio.m4a",
      durationMs,

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +42 to +50
const ios = Platform.OS === "ios";

const RECORDING_OPTIONS: RecordingOptions = {
isMeteringEnabled: false,
// Top-level now (see the note above). `ios`/`android` below re-state the extension because the
// platform blocks are spread OVER these on the way to the native module.
extension: ios ? ".wav" : ".m4a",
sampleRate: 16_000,
numberOfChannels: 1,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f96653 — good catch, and it was worse than a new-code slip: the expo-av original branched on Platform.OS === "ios" alone too, so Expo web has been labelling its audio/webm MediaRecorder output as .m4a / audio/mp4 all along. Porting made it visible because the new RECORDING_OPTIONS spells the web mimeType out next to the extension.

CONTAINER now keeps extension, contentType and fileName together across three cases (ios / web / android), so the label cannot drift from the bytes again.

One thing I deliberately did NOT fix, recorded in the file: web capture is still not end-to-end regardless of the label. The web recorder yields a blob: URL, and transcription.ts posts a uri through React Native's {uri, name, type} FormData extension, which is native-only. That gap predates this port.

Comment on lines +167 to +175
const player = useVideoPlayer(!src || isEmbed ? null : src);
const [started, setStarted] = useState(false);
useEffect(() => {
if (!player) return;
const sub = player.addListener("playingChange", ({ isPlaying }) => {
if (isPlaying) setStarted(true);
});
return () => sub.remove();
}, [player]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f96653 — correct, and it is reachable: useVideoPlayer memoizes on JSON.stringify(source), so a control whose Src changes IN PLACE gets a new player while started stayed true, and the new video would never show its poster. started is now reset with the player, in the same effect that subscribes to it, rather than only on unmount.

Pinned by a test that was verified non-vacuous: with setStarted(false) removed it fails ("brings the poster back when the Src changes after the first video played"), restored it passes.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 3)

    9 files   -  1      9 suites   - 1   4m 56s ⏱️ +10s
1 649 tests  - 44  1 643 ✅  - 48  6 💤 +4  0 ❌ ±0 
2 129 runs   - 44  2 123 ✅  - 48  6 💤 +4  0 ❌ ±0 

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 48 and adds 4 tests. Note that renamed tests count towards both.
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ JsonReaders_TolerateWrongKinds
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_AuthorNamePreferredOverAgentName
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_StreamingAssistantCell_WithAgentNameFallback
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_UserCell
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadThreadViewModel_ExtractsBubblesAndStatus
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadThreadViewModel_MissingFields_AreSafeDefaults
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadThreadViewModel_RawNode_DerivesExecutingFromStatus
MeshWeaver.Maui.Abstractions.Test.MauiComboboxFilterTest ‑ EmptyQuery_ReturnsAll_AndShowsList
MeshWeaver.Maui.Abstractions.Test.MauiComboboxFilterTest ‑ NoMatch_HidesList_AndIsEmpty
MeshWeaver.Maui.Abstractions.Test.MauiComboboxFilterTest ‑ Query_FiltersByCaseInsensitiveContains
…
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_BootsAndServesHealthy
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsMeshApi
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsNoBusinessRulesAssembly_ScopesComeFromThePlugin
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsPluginRegistry

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 0)

1 230 tests  +1   1 230 ✅ +5   12m 16s ⏱️ +56s
    9 suites ±0       0 💤  - 4 
    9 files   ±0       0 ❌ ±0 

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 4 and adds 5 tests. Note that renamed tests count towards both.
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_BootsAndServesHealthy
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsMeshApi
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsNoBusinessRulesAssembly_ScopesComeFromThePlugin
MeshWeaver.PluginImage.Test.PortalImageFacility ‑ InjectedImage_ShipsPluginRegistry
MeshWeaver.Search.Test.MarkdownParsingTest ‑ MarkdownFileParser_ParsesNewFormat
MeshWeaver.Search.Test.MarkdownParsingTest ‑ ParseContent_ReturnsMarkdownElement
MeshWeaver.Search.Test.MarkdownParsingTest ‑ ParseContent_WithMalformedYaml_ReturnsMarkdownElement
MeshWeaver.Search.Test.MarkdownParsingTest ‑ ParseContent_WithYaml_ReturnsMarkdownElement
MeshWeaver.Search.Test.MarkdownParsingTest ‑ ParseContent_WithoutYaml_ReturnsMarkdownElement

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 5)

1 233 tests  +16   1 232 ✅ +16   6m 44s ⏱️ -30s
   10 suites ± 0       1 💤 ± 0 
   10 files   ± 0       0 ❌ ± 0 

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 4 and adds 20 tests. Note that renamed tests count towards both.
MeshWeaver.Maui.Integration.Test.ChatSerializationContractTest ‑ MeshThread_Idle_IsNotExecuting
MeshWeaver.Maui.Integration.Test.ChatSerializationContractTest ‑ MeshThread_SerializesToTheKeysThreadChatViewProjects
MeshWeaver.Maui.Integration.Test.ChatSerializationContractTest ‑ ThreadMessage_StreamingAssistant_ProjectsToBubble
MeshWeaver.Maui.Integration.Test.ChatSerializationContractTest ‑ ThreadMessage_UserCompleted_NotStreaming
MeshWeaver.Hosting.PostgreSql.Test.SchemaInitCommandTimeoutTests ‑ EverySchemaInitCommand_UsesTheMaintenanceTimeout
MeshWeaver.Hosting.PostgreSql.Test.SchemaInitCommandTimeoutTests ‑ TheTimeoutIsGenerousButBounded
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Action_Throw
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ AsyncFunction_ThrowAsync
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ BeEquivalentTo
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Boolean
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Collection
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Comparable
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Dictionary
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ EmptyCompletion_StillReportsCompletedWithoutOne
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 4)

2 093 tests  +192   1 794 ✅  -   1   9m 37s ⏱️ -28s
    9 suites  -   1     298 💤 +192 
    9 files    -   1       1 ❌ +  1 

For more details on these failures, see this check.

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 5 and adds 197 tests. Note that renamed tests count towards both.
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ ALostWakeUpFailsTheJob_RatherThanWarning
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ DeclaringDependentsWithoutATokenIsPreflightedRed
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ NeitherTheGateNorTheDispatchCanBeSkippedOrDowngraded
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ TheWakeUpFiresOnlyAfterThePublication
MeshWeaver.Maui.E2E.Test.AppLaunchSmokeTest ‑ App_Launches_And_ShellSearchBox_IsPresent
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ TheGateCannotBeSkippedOrDowngraded
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ ThereIsNoDependentDispatch_AndNoCredentialForOne
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ EveryTool_RunsForReal_AndTheFileIsARealMeshNode
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ TheWorkingArea_IsScopedToItsOwnThread
MeshWeaver.Portal.E2E.BlazorVsNextCompareTest ‑ Compare_Blazor_Vs_Next_AcrossScreens
MeshWeaver.Portal.E2E.ChatAtAutocompleteTest ‑ TypingAt_SurfacesLocalPartitionNodes_AndSlashExpandsPartitions
MeshWeaver.Portal.E2E.ChatClearSkillNewComposerTest ‑ ClearSkill_InSidePanel_ReplacesThreadWithComposer_AndLeavesMainAlone
MeshWeaver.Portal.E2E.ChatComposerRefocusTest ‑ Composer_AcceptsTyping_AfterClickingAwayAndBack
MeshWeaver.Portal.E2E.ChatComposerSafariFocusReproTest ‑ Composer_AcceptsTyping_AfterFocusStealDuringStream
MeshWeaver.Portal.E2E.ChatComposerStreamingFocusTest ‑ Composer_KeepsFocus_WhileResponseStreams
…
This pull request removes 1 skipped test and adds 193 skipped tests. Note that renamed tests count towards both.
MeshWeaver.Maui.E2E.Test.AppLaunchSmokeTest ‑ App_Launches_And_ShellSearchBox_IsPresent
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ EveryTool_RunsForReal_AndTheFileIsARealMeshNode
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ TheWorkingArea_IsScopedToItsOwnThread
MeshWeaver.Portal.E2E.BlazorVsNextCompareTest ‑ Compare_Blazor_Vs_Next_AcrossScreens
MeshWeaver.Portal.E2E.ChatAtAutocompleteTest ‑ TypingAt_SurfacesLocalPartitionNodes_AndSlashExpandsPartitions
MeshWeaver.Portal.E2E.ChatClearSkillNewComposerTest ‑ ClearSkill_InSidePanel_ReplacesThreadWithComposer_AndLeavesMainAlone
MeshWeaver.Portal.E2E.ChatComposerRefocusTest ‑ Composer_AcceptsTyping_AfterClickingAwayAndBack
MeshWeaver.Portal.E2E.ChatComposerSafariFocusReproTest ‑ Composer_AcceptsTyping_AfterFocusStealDuringStream
MeshWeaver.Portal.E2E.ChatComposerStreamingFocusTest ‑ Composer_KeepsFocus_WhileResponseStreams
MeshWeaver.Portal.E2E.ChatComposerSwitchSelectionTest ‑ SlashCommands_OpenPicker_AndSelectionUpdatesStatusRow
MeshWeaver.Portal.E2E.ChatDelegationTest ‑ Coordinator_DelegatesToWorker_SubThreadSpawns_ResultFlowsBack_AndRendersWhenOpened
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 2)

3 214 tests   - 18   3 214 ✅  - 18   8m 27s ⏱️ +7s
    8 suites  -  1       0 💤 ± 0 
    8 files    -  1       0 ❌ ± 0 

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 18 tests.
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Action_Throw
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ AsyncFunction_ThrowAsync
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ BeEquivalentTo
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Boolean
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Collection
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Comparable
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Dictionary
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ EmptyCompletion_StillReportsCompletedWithoutOne
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Enum_HaveFlag
MeshWeaver.Reactive.Assertions.Test.AssertionTests ‑ Object_Be_NotBe_Null_OfType
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results (shard 1)

1 744 tests   - 195   1 744 ✅  -   2   9m 11s ⏱️ +11s
   10 suites ±  0       0 💤  - 193 
   10 files   ±  0       0 ❌ ±  0 

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 195 tests.
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ EveryTool_RunsForReal_AndTheFileIsARealMeshNode
MeshWeaver.Portal.E2E.AgentFilesToolingTest ‑ TheWorkingArea_IsScopedToItsOwnThread
MeshWeaver.Portal.E2E.BlazorVsNextCompareTest ‑ Compare_Blazor_Vs_Next_AcrossScreens
MeshWeaver.Portal.E2E.ChatAtAutocompleteTest ‑ TypingAt_SurfacesLocalPartitionNodes_AndSlashExpandsPartitions
MeshWeaver.Portal.E2E.ChatClearSkillNewComposerTest ‑ ClearSkill_InSidePanel_ReplacesThreadWithComposer_AndLeavesMainAlone
MeshWeaver.Portal.E2E.ChatComposerRefocusTest ‑ Composer_AcceptsTyping_AfterClickingAwayAndBack
MeshWeaver.Portal.E2E.ChatComposerSafariFocusReproTest ‑ Composer_AcceptsTyping_AfterFocusStealDuringStream
MeshWeaver.Portal.E2E.ChatComposerStreamingFocusTest ‑ Composer_KeepsFocus_WhileResponseStreams
MeshWeaver.Portal.E2E.ChatComposerSwitchSelectionTest ‑ SlashCommands_OpenPicker_AndSelectionUpdatesStatusRow
MeshWeaver.Portal.E2E.ChatDelegationTest ‑ Coordinator_DelegatesToWorker_SubThreadSpawns_ResultFlowsBack_AndRendersWhenOpened
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Test Results

    55 files   -  3      55 suites   - 3   51m 13s ⏱️ +26s
11 163 tests  - 48  10 857 ✅  - 48  305 💤  - 1  1 ❌ +1 
11 643 runs   - 48  11 337 ✅  - 48  305 💤  - 1  1 ❌ +1 

For more details on these failures, see this check.

Results for commit 0f96653. ± Comparison against base commit 7968b8e.

This pull request removes 52 and adds 4 tests. Note that renamed tests count towards both.
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ ALostWakeUpFailsTheJob_RatherThanWarning
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ DeclaringDependentsWithoutATokenIsPreflightedRed
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ NeitherTheGateNorTheDispatchCanBeSkippedOrDowngraded
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ TheWakeUpFiresOnlyAfterThePublication
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ JsonReaders_TolerateWrongKinds
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_AuthorNamePreferredOverAgentName
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_StreamingAssistantCell_WithAgentNameFallback
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadMessage_UserCell
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadThreadViewModel_ExtractsBubblesAndStatus
MeshWeaver.Maui.Abstractions.Test.MauiChatProjectionTest ‑ ReadThreadViewModel_MissingFields_AreSafeDefaults
…
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ TheGateCannotBeSkippedOrDowngraded
MeshWeaver.Documentation.Test.UpstreamBuildGateGuard ‑ ThereIsNoDependentDispatch_AndNoCredentialForOne
MeshWeaver.Hosting.PostgreSql.Test.SchemaInitCommandTimeoutTests ‑ EverySchemaInitCommand_UsesTheMaintenanceTimeout
MeshWeaver.Hosting.PostgreSql.Test.SchemaInitCommandTimeoutTests ‑ TheTimeoutIsGenerousButBounded

♻️ This comment has been updated with latest results.

1. The web recording was labelled with the WRONG container. Both this file and
   its expo-av predecessor branched on `Platform.OS === "ios"` alone, so Expo web
   — which records audio/webm through MediaRecorder, and always did — reached
   /api/speech/transcribe as `.m4a` / `audio/mp4`. The new RECORDING_OPTIONS spells
   the web mimeType out, which is what made the mismatch visible. `CONTAINER` now
   keeps extension, contentType and fileName together across three cases.
   (Web capture is still not end-to-end — the recorder yields a blob: URL and
   transcription.ts posts a uri through RN's native-only {uri,name,type} FormData
   extension. That gap predates the port and is recorded, not fixed, here.)

2. The Video poster's `started` flag was sticky. useVideoPlayer memoizes on the
   source, so a control whose Src changes IN PLACE gets a new player — but
   `started` stayed true and the new video's poster never appeared. Reset it with
   the player rather than only on unmount.

The second is pinned by a test that was verified non-vacuous: with setStarted(false)
removed it fails, restored it passes. vitest 16 files / 162 tests; tsc 0;
export + Playwright 5/5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Finish the Expo SDK migration for clients/react-native: RN 0.76.5 → ~0.82 and expo-av → expo-audio/expo-video (currently held back at SDK 52)

2 participants