//go:build browser && darwin package callstest import ( "bufio" "context" "encoding/json" "io" "net/http" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "sync" "testing" "time" ) // This gate uses the shipping Go bridge and WebRTC implementation with a // synthetic camera. Browser and native credentials come from real magic links. func TestNativeMacCallsBrowser(t *testing.T) { for _, guest := range []bool{false, true} { name := "member" if guest { name = "guest" } t.Run(name, func(t *testing.T) { nativeMacCallsBrowser(t, guest, false) }) } } func TestNativeMacChat(t *testing.T) { nativeMacCallsBrowser(t, false, true) } func nativeMacCallsBrowser(t *testing.T, guest, chat bool) { rig := newBrowserRig(t) room, token := rig.createMeeting(t) mode := "member" if guest { mode = "guest" rig.ctl.SetAllowlist(memberA) // This scenario's subject is the admission queue: a guest joins // from the app and a member admits them from the browser. Access // defaults ship the waiting room off, so it has to be asked for. rig.enableWaitingRoom(t, room) req, err := http.NewRequest("GET", rig.origin+"/j/"+token, nil) if err != nil { t.Fatal(err) } req.AddCookie(&http.Cookie{Name: rig.cookieName, Value: rig.cookieB}) res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } res.Body.Close() if res.StatusCode != 200 { t.Fatalf("guest link: %d", res.StatusCode) } } // Leave time for the chat exchange before exercising stream renewal. // Chat is intentionally not replayed to a detached recipient. if !chat { rig.ctl.SetStreamTimings(time.Second, 10*time.Second) } else { mode = "chat" } root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } ctx, cancel := context.WithTimeout(t.Context(), 160*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "node", filepath.Join(root, "test/browser/native.mjs"), rig.origin, room, binary, mode) cookies, _ := json.Marshal([]map[string]string{{"name": rig.cookieName, "value": rig.cookieB}}) fixture := map[string]string{"origin": rig.origin, "room": room, "cookies": string(cookies), "invitation": rig.origin + "/j/" + token} if chat { fixture["chatSnapshot"] = filepath.Join(root, "native/build/chat-incoming.png") } input, _ := json.Marshal(fixture) // CALLS_QUALITY_INTERVAL: the app's quality reports every two seconds // rather than thirty (spec §15.26), so the call holds several. cmd.Env = append(os.Environ(), "CALLS_COOKIE_NAME="+rig.cookieName, "CALLS_COOKIE_A="+rig.cookieA, "CALLS_NATIVE_INPUT="+string(input), "CALLS_QUALITY_INTERVAL=2") out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } cmd.Stderr = cmd.Stdout if err := cmd.Start(); err != nil { t.Fatal(err) } scanner := bufio.NewScanner(out) for scanner.Scan() { t.Log(scanner.Text()) } if err := cmd.Wait(); err != nil { t.Fatal(err) } assertNativeQualityReports(t, rig) } // assertNativeQualityReports is spec §15.26 from the app: it sent quality // reports while it was in the call, the server took them (a refused one is // never logged), and they carry what the app measured of the browser's // connection — its audio arriving, and the path it took. func assertNativeQualityReports(t *testing.T, rig *browserRig) { t.Helper() var mac []map[string]any for _, line := range rig.reports.lines("calls: quality") { if line["app"] == "mac" { mac = append(mac, line) } } if len(mac) == 0 { t.Fatalf("no quality report from the app; the report log holds:\n%s", rig.reports.raw()) } measured := false for _, line := range mac { if line["os"] != "macos" || line["osVersion"] == nil { t.Fatalf("the app's report does not say what it runs on: %v", line) } report, _ := line["report"].(map[string]any) peers, _ := report["peers"].([]any) for _, p := range peers { peer, _ := p.(map[string]any) in, _ := peer["in"].(map[string]any) audio, _ := in["audio"].(map[string]any) path, _ := peer["path"].(map[string]any) if packets, _ := audio["packets"].(float64); packets > 0 && path["protocol"] != nil { measured = true } } } if !measured { t.Fatalf("no report from the app measured the browser's audio and path: %v", mac) } b, _ := json.Marshal(mac[len(mac)-1]) t.Logf("PASS %d quality reports from the app; the last: %s", len(mac), b) } // TestNativeMacScreenShare is §5.4 between two apps, with no browser in // the call: each side is the shipping Go bridge and WebRTC build driving a // synthetic camera and a synthetic screen. The browser gate above holds // the app to the browser's rules in both directions; this holds two apps // to each other, which is the pairing nothing else would catch drifting — // and it shares from both sides at once, which is the negotiation // collision the browser's mutualshare scenario was written for. // // It is `make native-pair`, not part of `make native-browser`, because // two apps on one Mac do not reliably connect at all, screens or no // screens — measured with main's interop binary as well as this branch's, // before anything was shared. About two runs in five fail with signalling // complete and correctly ordered, every candidate added on both sides, and // no connectivity check ever answered. What the evidence points at is the // Mac rather than the code: the macOS application firewall was on, it // admits signed software automatically, and the interop binary is // unsigned, so inbound UDP reaches it only as a reply to its own outbound // on the same address pair — which two such binaries achieve only when // their first checks happen to cross in the right order. Chromium is // signed, so the browser gates never meet it, and a Developer ID Calls.app // would not either. Not proved: allowing the binary through the firewall // needs an administrator, and was not done. func TestNativeMacScreenShare(t *testing.T) { rig := newBrowserRig(t) room, _ := rig.createMeeting(t) root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } ctx, cancel := context.WithTimeout(t.Context(), 140*time.Second) defer cancel() // The person behind cookie A is paul@example.com, and the hub names // a person by the local part of their address. const paulsScreen = "paul\u2019s screen" // The first app shares before the second has joined, so the `share` // message goes to nobody and is never replayed: the second app has // only the m-line to go on, and has to caption the screen anyway. a := startNative(ctx, t, binary, rig, room, rig.cookieA, "a") defer a.leave(t) waitNative(t, "the first app is in the meeting", func() bool { return a.sample().Phase == "admitted" }, a) a.command(t, "screen-on") waitNative(t, "the first app is sharing to an empty room", func() bool { s := a.sample() return s.SharingScreen != nil && *s.SharingScreen }, a) b := startNative(ctx, t, binary, rig, room, rig.cookieB, "b") defer b.leave(t) waitNative(t, "both apps decode each other's camera", func() bool { return a.stat("framesDecoded") > 15 && b.stat("framesDecoded") > 15 }, a, b) waitNative(t, "the second app captions the first app's screen", func() bool { c := b.sample().ScreenCaptions return len(c) == 1 && c[0] == paulsScreen && b.stat("remoteScreens") == 1 }, a, b) base, camera := b.stat("screenFramesDecoded"), b.stat("framesDecoded") waitNative(t, "the second app decodes the screen and the camera beside it", func() bool { return b.stat("screenFramesDecoded") > base+15 && b.stat("framesDecoded") > camera+15 }, a, b) if b.stat("remoteTracks") != 1 { t.Fatalf("the screen was taken for a second camera: %+v", b.sample()) } a.command(t, "screen-off") waitNative(t, "the screen goes when the first app stops", func() bool { return len(b.sample().ScreenCaptions) == 0 && b.stat("remoteScreens") == 0 }, a, b) camera = b.stat("framesDecoded") waitNative(t, "the camera survives the screen stopping", func() bool { return b.stat("framesDecoded") > camera+15 }, a, b) t.Log("PASS app to app: a screen shared before the other app joined is captioned, decoded and stopped by the button") // The other way, ended by the capture itself. b.command(t, "screen-on") waitNative(t, "the first app shows the second app's screen", func() bool { return len(a.sample().ScreenCaptions) == 1 && a.stat("remoteScreens") == 1 }, a, b) base = a.stat("screenFramesDecoded") waitNative(t, "the first app decodes it", func() bool { return a.stat("screenFramesDecoded") > base+15 }, a, b) b.command(t, "screen-end") waitNative(t, "a capture ending on its own ends the share", func() bool { s := b.sample() return s.SharingScreen != nil && !*s.SharingScreen && len(a.sample().ScreenCaptions) == 0 && a.stat("remoteScreens") == 0 }, a, b) t.Log("PASS app to app the other way, ended by the capture") // Both at once: two transceivers added in the same moment, and a // collision for perfect negotiation to settle without either screen // landing on the m-line the other side built for something else. a.command(t, "screen-on") b.command(t, "screen-on") waitNative(t, "each app shows the other's screen", func() bool { return a.stat("remoteScreens") == 1 && b.stat("remoteScreens") == 1 }, a, b) baseA, baseB := a.stat("screenFramesDecoded"), b.stat("screenFramesDecoded") camA, camB := a.stat("framesDecoded"), b.stat("framesDecoded") waitNative(t, "both screens and both cameras decode through the collision", func() bool { return a.stat("screenFramesDecoded") > baseA+15 && b.stat("screenFramesDecoded") > baseB+15 && a.stat("framesDecoded") > camA+15 && b.stat("framesDecoded") > camB+15 }, a, b) if a.stat("remoteTracks") != 1 || b.stat("remoteTracks") != 1 { t.Fatalf("a screen was taken for a camera: a=%+v b=%+v", a.sample(), b.sample()) } a.command(t, "screen-off") b.command(t, "screen-off") waitNative(t, "both screens go", func() bool { return a.stat("remoteScreens") == 0 && b.stat("remoteScreens") == 0 && len(a.sample().ScreenCaptions) == 0 && len(b.sample().ScreenCaptions) == 0 }, a, b) t.Log("PASS app to app, both sharing at once") } type nativeSample struct { Phase string `json:"phase"` Peer string `json:"peer"` Stats map[string]uint64 `json:"stats"` ScreenCaptions []string `json:"screenCaptions"` SharingScreen *bool `json:"sharingScreen"` ScreenRecovery string `json:"screenRecovery"` Diagnostics string `json:"diagnostics"` } // nativeApp is one CallsInterop process and the last observation it // printed. Only numeric samples are kept; nothing else it prints is read. type nativeApp struct { label string cmd *exec.Cmd stdin io.WriteCloser mu sync.Mutex last nativeSample diag string done chan struct{} } func startNative(ctx context.Context, t *testing.T, binary string, rig *browserRig, room, cookie, label string) *nativeApp { t.Helper() cookies, _ := json.Marshal([]map[string]string{{"name": rig.cookieName, "value": cookie}}) input, _ := json.Marshal(map[string]string{"origin": rig.origin, "room": room, "cookies": string(cookies)}) cmd := exec.CommandContext(ctx, binary) stdin, err := cmd.StdinPipe() if err != nil { t.Fatal(err) } out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } app := &nativeApp{label: label, cmd: cmd, stdin: stdin, done: make(chan struct{})} go func() { defer close(app.done) scanner := bufio.NewScanner(out) for scanner.Scan() { var s nativeSample if json.Unmarshal(scanner.Bytes(), &s) != nil { continue } if s.Diagnostics != "" { app.mu.Lock() app.diag = s.Diagnostics app.mu.Unlock() continue } if s.Stats == nil { continue } app.mu.Lock() app.last = s app.mu.Unlock() } }() if _, err := io.WriteString(stdin, string(input)+"\n"); err != nil { t.Fatal(err) } return app } func (n *nativeApp) sample() nativeSample { n.mu.Lock() defer n.mu.Unlock() return n.last } func (n *nativeApp) stat(name string) uint64 { return n.sample().Stats[name] } // diagnostics asks for "Copy call diagnostics" and returns what came // back, or "" if nothing did within five seconds. func (n *nativeApp) diagnostics(t *testing.T) string { t.Helper() n.mu.Lock() n.diag = "" n.mu.Unlock() n.command(t, "diagnostics") for until := time.Now().Add(5 * time.Second); time.Now().Before(until); time.Sleep(100 * time.Millisecond) { n.mu.Lock() d := n.diag n.mu.Unlock() if d != "" { return d } } return "" } func (n *nativeApp) command(t *testing.T, line string) { t.Helper() if _, err := io.WriteString(n.stdin, line+"\n"); err != nil { t.Fatalf("%s: %s: %v", n.label, line, err) } } func (n *nativeApp) leave(t *testing.T) { _, _ = io.WriteString(n.stdin, "leave\n") select { case <-n.done: case <-time.After(5 * time.Second): _ = n.cmd.Process.Kill() } _ = n.cmd.Wait() } // waitNative polls until ok holds, and on failure says what each app // last reported, which is the only account of the failure there is. func waitNative(t *testing.T, label string, ok func() bool, apps ...*nativeApp) { t.Helper() until := time.Now().Add(35 * time.Second) for time.Now().Before(until) { if ok() { return } time.Sleep(250 * time.Millisecond) } for _, app := range apps { t.Logf("%s: %+v", app.label, app.sample()) } t.Fatal(label) } // TestNativeMacScreenMotion is the app sharing a 1080p page that scrolls // at 30 fps, measured at the browser: frames a second and frame size on // the received screen, for each "Optimise for" mode, on an open link and // under a capped uplink. It holds motion and the balanced default to a // frame rate a scroll can be followed at, and detail to full resolution // (test/browser/native-motion.mjs has the thresholds). func TestNativeMacScreenMotion(t *testing.T) { rig := newBrowserRig(t) room, _ := rig.createMeeting(t) root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } ctx, cancel := context.WithTimeout(t.Context(), 300*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "node", filepath.Join(root, "test/browser/native-motion.mjs"), rig.origin, room, binary) cookies, _ := json.Marshal([]map[string]string{{"name": rig.cookieName, "value": rig.cookieB}}) input, _ := json.Marshal(map[string]string{"origin": rig.origin, "room": room, "cookies": string(cookies)}) cmd.Env = append(os.Environ(), "CALLS_COOKIE_NAME="+rig.cookieName, "CALLS_COOKIE_A="+rig.cookieA, "CALLS_NATIVE_INPUT="+string(input)) out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } cmd.Stderr = cmd.Stdout if err := cmd.Start(); err != nil { t.Fatal(err) } scanner := bufio.NewScanner(out) for scanner.Scan() { t.Log(scanner.Text()) } if err := cmd.Wait(); err != nil { t.Fatal(err) } } // TestNativeMacScreenRecovery is Paul's report of 2026-09-28 — "after a // while, my screen froze on her end … I had to stop and re-share" — held // as a gate: the app shares its screen to a browser while the call is // disturbed underneath it, and the browser's received screen has to keep // decoding, or come back within a bound, with nobody sharing again. // test/browser/native-screen.mjs has the scenarios and their thresholds. // // renewals is the one that reproduced it: a relay configured (so /ice // carries a renewAt) and the stream renewed every ten seconds, which is // the production shape in miniature — every renewal answered with a // roster, every roster with a fresh /ice, and up to 0.4.1 every fresh // /ice with an ICE restart on every connection. func TestNativeMacScreenRecovery(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } scenarios := []string{"renewals", "resume", "capture", "encoder", "loss", "wedge", "wedge-sticky", "web-wedge", "web-wedge-sticky"} if only := os.Getenv("SCREEN_SCENARIOS"); only != "" { scenarios = strings.Split(only, ",") } for _, scenario := range scenarios { t.Run(scenario, func(t *testing.T) { rig := newBrowserRig(t) soaking := scenario == "soak" || scenario == "web-soak" if scenario == "renewals" || soaking { if err := rig.ctl.SetRelay(browserRelayContract()); err != nil { t.Fatal(err) } rig.ctl.SetIceLife(20 * time.Second) } if scenario == "renewals" || scenario == "resume" || soaking { rig.ctl.SetStreamTimings(time.Second, 10*time.Second) } room, _ := rig.createMeeting(t) limit := 240 * time.Second if soaking { // SCREEN_SOAK_SECONDS of soak, and the scenario around it. seconds, _ := strconv.Atoi(envOr("SCREEN_SOAK_SECONDS", "60")) limit = time.Duration(seconds)*time.Second + 180*time.Second } ctx, cancel := context.WithTimeout(t.Context(), limit) defer cancel() cmd := exec.CommandContext(ctx, "node", filepath.Join(root, "test/browser/native-screen.mjs"), rig.origin, room, binary, scenario) cookies, _ := json.Marshal([]map[string]string{{"name": rig.cookieName, "value": rig.cookieB}}) input, _ := json.Marshal(map[string]string{"origin": rig.origin, "room": room, "cookies": string(cookies)}) cmd.Env = append(os.Environ(), "CALLS_COOKIE_NAME="+rig.cookieName, "CALLS_COOKIE_A="+rig.cookieA, "CALLS_NATIVE_INPUT="+string(input)) out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } cmd.Stderr = cmd.Stdout if err := cmd.Start(); err != nil { t.Fatal(err) } scanner := bufio.NewScanner(out) for scanner.Scan() { t.Log(scanner.Text()) } if err := cmd.Wait(); err != nil { t.Fatal(err) } }) } } // TestNativeMacScreenPairRecovery is TestNativeMacScreenRecovery between // two apps, which is what the report of 2026-09-28 actually was: Paul on // the Mac app sharing, Vicky watching on an app. App A shares, app B // watches, and B's received screen has to keep decoding, or come back // within a bound, through the same disturbances. // // SCREEN_PAIR_A and SCREEN_PAIR_B name other CallsInterop binaries for // either side — an older build, say — so mixed versions can be held to // the same rules; a scenario that needs a command the older side does // not have is skipped for that pairing. SCREEN_PAIR_DISPLAY=1 shares // this Mac's real display (Screen Recording permission needed) rather // than the synthetic screen, which puts ScreenCaptureKit and the // hardware encoder in the path. // // It is in `make native-pair`, and flaky for the same reason as // TestNativeMacScreenShare: two unsigned apps on one Mac do not always // connect at all. func TestNativeMacScreenPairRecovery(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } current := filepath.Join(root, "native/apple/.build/debug/CallsInterop") binA, binB := current, current if v := os.Getenv("SCREEN_PAIR_A"); v != "" { binA = v } if v := os.Getenv("SCREEN_PAIR_B"); v != "" { binB = v } for _, b := range []string{binA, binB} { if _, err := os.Stat(b); err != nil { t.Fatalf("no interop executable at %s: build with make native-mac first", b) } } soak := 45 * time.Second if v, err := time.ParseDuration(os.Getenv("SCREEN_PAIR_SOAK")); err == nil { soak = v } scenarios := []string{"renewals", "capture", "encoder", "loss", "wedge", "wedge-sticky", "reshare"} if only := os.Getenv("SCREEN_SCENARIOS"); only != "" { scenarios = strings.Split(only, ",") } newA, newB := binA == current, binB == current for _, scenario := range scenarios { t.Run(scenario, func(t *testing.T) { switch { case (scenario == "capture" || scenario == "encoder") && !newA: t.Skip("the sharing app predates the stall commands") case scenario == "loss" && (!newA || !newB): t.Skip("a stall report needs the new receiver and the new sharer") case strings.HasPrefix(scenario, "wedge") || scenario == "reshare": if !newA || !newB { t.Skip("the escalation needs the new receiver and the new sharer") } // Decoders the receiver can wedge (spec §15.22). t.Setenv("CALLS_DECODER_WEDGE", "1") } rig := newBrowserRig(t) if scenario == "renewals" || scenario == "soak" { if err := rig.ctl.SetRelay(browserRelayContract()); err != nil { t.Fatal(err) } rig.ctl.SetIceLife(20 * time.Second) rig.ctl.SetStreamTimings(time.Second, 10*time.Second) } room, _ := rig.createMeeting(t) ctx, cancel := context.WithTimeout(t.Context(), soak+150*time.Second) defer cancel() // Two unsigned apps on one Mac fail to connect at all about two // times in five (see TestNativeMacScreenShare), before anything // this test is about has happened; that gets three tries. var a, b *nativeApp for attempt := 1; ; attempt++ { a = startNative(ctx, t, binA, rig, room, rig.cookieA, "a") waitNative(t, "the sharing app is in", func() bool { return a.sample().Phase == "admitted" }, a) b = startNative(ctx, t, binB, rig, room, rig.cookieB, "b") if pollNative(35*time.Second, func() bool { return a.stat("framesDecoded") > 15 && b.stat("framesDecoded") > 15 }) { break } a.leave(t) b.leave(t) if attempt == 3 { t.Fatal("the two apps never connected") } t.Logf("RETRY the two apps did not connect (attempt %d)", attempt) } defer a.leave(t) defer b.leave(t) a.command(t, "screen-mode "+envOr("SCREEN_MODE", "balanced")) if os.Getenv("SCREEN_PAIR_DISPLAY") == "1" && newA { a.command(t, "screen-display 6") } else if os.Getenv("SCREEN_SOURCE") == "motion" { // The 1920-wide scrolling page (CALLS_SYNTHETIC_SCREEN_HEIGHT). a.command(t, "screen-motion") } else { a.command(t, "screen-on") } waitNative(t, "the watching app decodes the screen", func() bool { return b.stat("remoteScreens") == 1 && b.stat("screenFramesDecoded") > 15 }, a, b) // The watch: B's screen frames, once a second. freezes, longest := 0, time.Duration(0) watch := func(d time.Duration, label string) { last, moved, frozen := b.stat("screenFramesDecodedTotal"), time.Now(), false camera := b.stat("framesDecoded") for until := time.Now().Add(d); time.Now().Before(until); { time.Sleep(time.Second) now := b.stat("screenFramesDecodedTotal") if now > last { if frozen { gap := time.Since(moved) t.Logf("FREEZE %s: %v from %.3f, then frames again (b's camera decoded %d frames meanwhile)", label, gap.Round(time.Millisecond), float64(moved.UnixMilli())/1000, b.stat("framesDecoded")-camera) freezes++ longest = max(longest, gap) frozen = false } moved = time.Now() camera = b.stat("framesDecoded") } else if !frozen && time.Since(moved) > 3*time.Second { frozen = true s := a.sample().Stats t.Logf("FROZEN %s: no screen frame at b for %v (a: captured %d encoded %d sent %d, watchdog k%d e%d r%d a%d rebuilds %d shares again %d)", label, time.Since(moved).Round(time.Millisecond), s["screenCaptureDelivered"], s["screenFramesEncoded"], s["screenFramesSent"], s["screenWatchdogKeyframes"], s["screenWatchdogEncoderRecoveries"], s["screenWatchdogCaptureRestarts"], s["screenWatchdogReAdds"], s["screenWatchdogRebuilds"], s["screenWatchdogReshares"]) } last = now } if frozen { t.Errorf("%s: b's screen froze and did not come back", label) } } recovered := func(label string) { base := b.stat("screenFramesDecoded") until := time.Now().Add(15 * time.Second) for time.Now().Before(until) { if b.stat("screenFramesDecoded") > base+10 { return } time.Sleep(250 * time.Millisecond) } t.Fatalf("%s: a=%+v b=%+v", label, a.sample(), b.sample()) } switch scenario { case "soak": // §15.22's search for the cause, between two apps: renewals // and resumes as `renewals`, with A's mode cycled and its // uplink capped and lifted every 45 s, for SCREEN_PAIR_SOAK. modes := []string{"balanced", "motion", "detail"} uplinks := []string{"0", "1500000", "600000", "0", "3000000", "900000"} done := make(chan struct{}) notices := map[string]bool{} var mu sync.Mutex go func() { step := 0 tick := time.NewTicker(45 * time.Second) defer tick.Stop() for { select { case <-done: return case <-tick.C: step++ a.command(t, "screen-mode "+modes[step%len(modes)]) a.command(t, "uplink "+uplinks[step%len(uplinks)]) t.Logf("soak step %d: mode %s, uplink %s", step, modes[step%len(modes)], uplinks[step%len(uplinks)]) } } }() go func() { for { select { case <-done: return case <-time.After(500 * time.Millisecond): if n := a.sample().ScreenRecovery; n != "" { mu.Lock() notices[n] = true mu.Unlock() } } } }() watch(soak, "soak") close(done) a.command(t, "uplink 0") s := a.sample().Stats t.Logf("SOAK a: ICE restarts %d (b %d), stall reports %d, keyframes %d, fresh encoders %d, re-adds %d, rebuilds %d, shares again %d, given up %d, capture restarts %d; b's pictures attached again %d", s["iceRestarts"], b.stat("iceRestarts"), s["screenStallReports"], s["screenWatchdogKeyframes"], s["screenWatchdogEncoderRecoveries"], s["screenWatchdogReAdds"], s["screenWatchdogRebuilds"], s["screenWatchdogReshares"], s["screenWatchdogFailures"], s["screenWatchdogCaptureRestarts"], b.stat("screenReattaches")) mu.Lock() t.Logf("SOAK notices at a: %q; %d freezes, longest %v", keys(notices), freezes, longest) mu.Unlock() t.Logf("a's diagnostics:\n%s", screenSections(a.diagnostics(t))) t.Logf("b's diagnostics:\n%s", screenSections(b.diagnostics(t))) if s["screenWatchdogFailures"] > 0 { t.Error("a gave up on b's view") } case "renewals": watch(soak, "renewals") t.Logf("ICE restarts: a started %d, b started %d", a.stat("iceRestarts"), b.stat("iceRestarts")) if newA && (a.stat("iceRestarts") < 1 || a.stat("iceRestarts") > uint64(soak/(16*time.Second))+1) { t.Errorf("the sharing app restarted ICE %d times; renewals fall due every ~17 s", a.stat("iceRestarts")) } // Bounded, not zero: between two apps an ICE restart now and // then (about one in ten here) stops the screen — and only the // screen, the camera keeps moving — for three or four seconds, // which was not traced further on 2026-09-28. 0.4.x restarted // ICE on every stream resume, every five minutes; now it is // every renewal, two and a half hours apart, and the watchdogs // are behind it. A freeze that outlasts them fails. if longest > 6*time.Second { t.Errorf("b's screen froze %d times, longest %v", freezes, longest) } case "capture": watch(5*time.Second, "before") a.command(t, "screen-stall") recovered("a stalled capture is restarted") if a.stat("screenWatchdogCaptureRestarts") == 0 { t.Error("the watchdog did not say it restarted the capture") } case "encoder": watch(5*time.Second, "before") a.command(t, "screen-encoder-stall") time.Sleep(time.Second) recovered("a stalled encoder is recovered") if a.stat("screenWatchdogKeyframes")+a.stat("screenWatchdogEncoderRecoveries") == 0 { t.Error("the watchdog did not say it recovered the encoder") } case "loss": watch(5*time.Second, "before") before := a.stat("screenWatchdogKeyframes") b.command(t, "screen-freeze-test 8000") waitNative(t, "b reports a's screen stalled", func() bool { return b.stat("screenStalledHere") == 1 }, a, b) waitNative(t, "a hears it and answers with a keyframe", func() bool { return a.stat("screenStallReports") > 0 && a.stat("screenWatchdogKeyframes") > before }, a, b) waitNative(t, "b clears its report once frames move", func() bool { return b.stat("screenStalledHere") == 0 }, a, b) recovered("b decodes after the keyframe") case "wedge": // B's screen decoder stops putting out pictures while packets // keep arriving (spec §15.22): keyframes cannot mend it and a // new transceiver can. Without a manual share again, B has to // be decoding again within 30 s, and A's notice has to have // said so honestly on the way. watch(5*time.Second, "before") keyframes := a.stat("screenWatchdogKeyframes") wedged := time.Now() b.command(t, "screen-decoder-wedge") waitNative(t, "b reports a's screen stalled", func() bool { return b.stat("screenStalledHere") == 1 }, a, b) notices := map[string]bool{} base := b.stat("screenFramesDecodedTotal") back := false for until := wedged.Add(30 * time.Second); time.Now().Before(until); time.Sleep(250 * time.Millisecond) { if n := a.sample().ScreenRecovery; n != "" { notices[n] = true } if b.stat("screenFramesDecodedTotal") > base+15 { back = true break } } took := time.Since(wedged) t.Logf("b decoding again after %v: rebuilds %d, shares again %d, keyframes %d, b's pictures attached again %d", took.Round(time.Millisecond), a.stat("screenWatchdogRebuilds"), a.stat("screenWatchdogReshares"), a.stat("screenWatchdogKeyframes")-keyframes, b.stat("screenReattaches")) if !back { t.Logf("a:\n%s", screenSections(a.diagnostics(t))) t.Logf("b:\n%s", screenSections(b.diagnostics(t))) t.Fatalf("b's wedged screen was not restored within 30 s: a=%+v b=%+v", a.sample(), b.sample()) } if a.stat("screenWatchdogKeyframes") == keyframes { t.Error("keyframes were not tried first") } if a.stat("screenWatchdogRebuilds") == 0 { t.Error("b decoded again without a rebuild: the wedge did not hold") } waitNative(t, "b clears its report", func() bool { return b.stat("screenStalledHere") == 0 }, a, b) waitNative(t, "a's notice clears", func() bool { n := a.sample().ScreenRecovery if n != "" { notices[n] = true } return n == "" }, a, b) t.Logf("notices seen at a: %q", keys(notices)) if !anyHas(notices, "recovering") || !anyHas(notices, "Restarting your share for") { t.Errorf("the notice did not move from recovering to restarting: %q", keys(notices)) } watch(10*time.Second, "after the rebuild") t.Logf("a's diagnostics:\n%s", screenSections(a.diagnostics(t))) t.Logf("b's diagnostics:\n%s", screenSections(b.diagnostics(t))) case "wedge-sticky": // Every screen decoder B makes is wedged, the new ones too: // nothing A does can mend it, and A has to say so rather than // sit at "recovering". watch(5*time.Second, "before") b.command(t, "screen-decoder-wedge sticky") notices := map[string]bool{} gaveUp := false for until := time.Now().Add(60 * time.Second); time.Now().Before(until); time.Sleep(250 * time.Millisecond) { if n := a.sample().ScreenRecovery; n != "" { notices[n] = true } if a.stat("screenWatchdogFailures") > 0 && anyHas(map[string]bool{a.sample().ScreenRecovery: true}, "Couldn\u2019t restore") { gaveUp = true break } } t.Logf("notices seen at a: %q", keys(notices)) if !gaveUp { t.Fatalf("a never said it could not restore b's view: a=%+v", a.sample()) } if a.stat("screenWatchdogRebuilds") == 0 || a.stat("screenWatchdogReshares") == 0 { t.Errorf("a gave up without trying a rebuild and a share again: %+v", a.sample().Stats) } if !anyHas(notices, "Restarting your share for") { t.Errorf("the notice never said it was restarting: %q", keys(notices)) } // The wedge lifted, the view comes back and the notice goes. b.command(t, "screen-decoder-release") recoveredTotal := func() bool { base := b.stat("screenFramesDecodedTotal") return pollNative(20*time.Second, func() bool { return b.stat("screenFramesDecodedTotal") > base+10 }) } if !recoveredTotal() { t.Fatalf("b did not decode again once the wedge was lifted: %+v", b.sample()) } waitNative(t, "a's notice clears once b's view is back", func() bool { return a.sample().ScreenRecovery == "" }, a, b) t.Logf("a's diagnostics:\n%s", screenSections(a.diagnostics(t))) case "reshare": // The watchdog's share again, on its own: every connection's // screen stopped and added again from the same capture, and // the watcher decoding again within a bound, one tile. watch(5*time.Second, "before") base := b.stat("screenFramesDecodedTotal") started := time.Now() a.command(t, "screen-reshare-test") if !pollNative(15*time.Second, func() bool { return b.stat("screenFramesDecodedTotal") > base+30 }) { t.Fatalf("b did not decode again after the share again: a=%+v b=%+v", a.sample(), b.sample()) } t.Logf("b decoding again %v after the share again", time.Since(started).Round(time.Millisecond)) waitNative(t, "one screen at b", func() bool { return b.stat("remoteScreens") == 1 && len(b.sample().ScreenCaptions) == 1 }, a, b) if a.sample().SharingScreen == nil || !*a.sample().SharingScreen { t.Error("a stopped sharing") } watch(10*time.Second, "after the share again") } t.Logf("PASS %s: %d freezes, longest %v", scenario, freezes, longest) }) } } // screenSections is the screen's part of "Copy call diagnostics": the // "## Screen" sections and nothing else. func screenSections(diag string) string { var out []string in := false for _, line := range strings.Split(diag, "\n") { if strings.HasPrefix(line, "## ") { in = strings.HasPrefix(line, "## Screen") } if in { out = append(out, line) } } return strings.Join(out, "\n") } func keys(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } func anyHas(m map[string]bool, part string) bool { for k := range m { if strings.Contains(k, part) { return true } } return false } // pollNative is waitNative without the failure: whether ok held in time. func pollNative(d time.Duration, ok func() bool) bool { for until := time.Now().Add(d); time.Now().Before(until); time.Sleep(250 * time.Millisecond) { if ok() { return true } } return false } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // TestNativeMacBarLayout holds the full-window call's control bar whole // and centred at every window size (2026-10-03: Paul's 0.4.6 window // showed it off the left edge with mute and camera cut off). It needs no // server: CallsInterop draws the call from fixtures in a real titled // window, resizes it live, animated and below the app's minimum, and // checks the layout and what was drawn (BarLayoutHarness.swift). // CALLS_BAR_SNAPSHOTS keeps the pictures. func TestNativeMacBarLayout(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } folder := os.Getenv("CALLS_BAR_SNAPSHOTS") if folder == "" { folder = t.TempDir() } ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute) defer cancel() out, err := exec.CommandContext(ctx, binary, "--bar-layout", folder).CombinedOutput() t.Log(string(out)) if err != nil { t.Fatalf("bar layout: %v", err) } } // TestNativeMacWebKit is the Mac app in a call with Safari's engine // (Playwright's WebKit, which is not iOS Safari: see native-webkit.mjs // for what it can and cannot stand in for). 2026-10-03: a call between // the Mac app and an iPhone in Safari "kept cutting in and out". It is // `make native-webkit`, apart from native-browser, because it needs // Playwright's WebKit installed. func TestNativeMacWebKit(t *testing.T) { rig := newBrowserRig(t) room, token := rig.createMeeting(t) root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with make native-mac first") } ctx, cancel := context.WithTimeout(t.Context(), 180*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "node", filepath.Join(root, "test/browser/native-webkit.mjs"), rig.origin, room, binary) cookies, _ := json.Marshal([]map[string]string{{"name": rig.cookieName, "value": rig.cookieB}}) input, _ := json.Marshal(map[string]string{"origin": rig.origin, "room": room, "cookies": string(cookies), "invitation": rig.origin + "/j/" + token}) cmd.Env = append(os.Environ(), "CALLS_COOKIE_NAME="+rig.cookieName, "CALLS_COOKIE_A="+rig.cookieA, "CALLS_NATIVE_INPUT="+string(input)) out, err := cmd.CombinedOutput() t.Log(string(out)) if err != nil { t.Fatalf("native-webkit: %v", err) } }