oficina / meet Public

Repair the gates the rename merge left pointing at nothing

Three failures, one cause each, all of them silent in the sense that
mattered: the gate went red or green for reasons that had nothing to do
with the code under it.

Every test/browser/*.test.mjs read its subject from internal/meet/static,
which has not existed since the rename. The rename branch had fixed all
six; the merge into main resolved those files to main's side and took the
old paths back. That is the whole web gate — each node test dies on the
open before Playwright is reached, so `make browser` could not run at all.

native-auth.mjs filled input[name="address"] on whatever page the target
landed on. Since guest joining, an invitation lands on guest.html — join
in the app, a name form, a link to sign in — and that page has no address
field, so the guest scenario spent thirty seconds waiting for one. It now
follows the sign-in link when that is the page it got, and goes straight
to the field when it is already there.

The native gate's guest case then got further and stopped on an Admit
button that was never coming: meeting access defaults ship the waiting
room off, so a guest following a link is admitted at once and no queue is
drawn. A scenario whose subject IS the queue has to ask for one, or it
quietly stops testing the thing it is named after. rig.enableWaitingRoom
turns it on, and deliberately does not use http.DefaultClient — that
follows the 303 to the settings page and hands back its 200, which makes
a refused save and an accepted one look identical.

Gates: make ci, make native-test, make native-browser and make browser all
green, the last two including both guest cases. Run make ci with
GOTOOLCHAIN=go1.26.5; under any other toolchain the credits test fails on
runtime.Version() and reports it as a credits problem, which it is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev eb15233e0e49f0474b367eb04a60ecf35b6653f4 parent 4470a38
9 files changed, +62 −9
  • internal/callstest/browser_test.go +37 −0
  • internal/callstest/native_browser_test.go +4 −0
  • test/browser/audio.test.mjs +2 −2
  • test/browser/chat.test.mjs +1 −1
  • test/browser/devices.test.mjs +2 −2
  • test/browser/native-auth.mjs +13 −1
  • test/browser/negotiation.test.mjs +1 −1
  • test/browser/room-lane.test.mjs +1 −1
  • test/browser/room-share.test.mjs +1 −1
diff --git a/internal/callstest/browser_test.go b/internal/callstest/browser_test.go
index f3e6777..d139c84 100644
--- a/internal/callstest/browser_test.go
+++ b/internal/callstest/browser_test.go
@@ -506,6 +506,43 @@ func (rig *browserRig) signIn(t *testing.T, address string) (name, value string)
var sqidOnPage = regexp.MustCompile(`/m/([23456789a-z]{10})/join`)
var tokenOnPage = regexp.MustCompile(`/j/([A-Za-z0-9_-]{43})`)
+// enableWaitingRoom turns one meeting's waiting room on, as member A.
+//
+// It exists because the waiting room is no longer the default: meeting
+// access defaults ship it off, so a guest following a link is admitted
+// straight away and no admission queue is ever drawn. A scenario whose
+// subject IS the queue has to ask for one, or it silently stops testing
+// what it is named after — which is how the native gate's guest case came
+// to wait thirty seconds for an Admit button that was never going to be
+// there.
+func (rig *browserRig) enableWaitingRoom(t *testing.T, sqid string) {
+ t.Helper()
+ req, err := http.NewRequest(http.MethodPost, rig.origin+"/meetings/"+sqid+"/settings",
+ strings.NewReader(url.Values{"waiting_room": {"on"}}.Encode()))
+ if err != nil {
+ t.Fatalf("build settings request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Origin", rig.origin)
+ req.AddCookie(&http.Cookie{Name: rig.cookieName, Value: rig.cookieA})
+ // Not http.DefaultClient: it follows the 303 to the settings page and
+ // hands back that page's 200, so a refused save and an accepted one
+ // look identical. The redirect itself is the only honest evidence the
+ // handler accepted this.
+ client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
+ return http.ErrUseLastResponse
+ }}
+ res, err := client.Do(req)
+ if err != nil {
+ t.Fatalf("POST meeting settings: %v", err)
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusSeeOther {
+ body, _ := io.ReadAll(res.Body)
+ t.Fatalf("POST meeting settings: %d\n%s", res.StatusCode, body)
+ }
+}
+
// createMeeting is member A starting a call and reading the link off the
// page it is shown on exactly once.
func (rig *browserRig) createMeeting(t *testing.T) (sqid, token string) {
diff --git a/internal/callstest/native_browser_test.go b/internal/callstest/native_browser_test.go
index 2f4d206..51d90e9 100644
--- a/internal/callstest/native_browser_test.go
+++ b/internal/callstest/native_browser_test.go
@@ -36,6 +36,10 @@ func nativeMacCallsBrowser(t *testing.T, guest, chat bool) {
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)
diff --git a/test/browser/audio.test.mjs b/test/browser/audio.test.mjs
index fe83d1e..328a5a5 100644
--- a/test/browser/audio.test.mjs
+++ b/test/browser/audio.test.mjs
@@ -21,8 +21,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const devicesJS = fileURLToPath(new URL('../../internal/meet/static/devices.js', import.meta.url));
-const roomJS = fileURLToPath(new URL('../../internal/meet/static/room.js', import.meta.url));
+const devicesJS = fileURLToPath(new URL('../../internal/calls/static/devices.js', import.meta.url));
+const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
// lift returns the source of one `function name(...) {...}` by matching
// braces from its declaration — negotiation.test.mjs's helper, and it
diff --git a/test/browser/chat.test.mjs b/test/browser/chat.test.mjs
index e0f5a0f..75d7973 100644
--- a/test/browser/chat.test.mjs
+++ b/test/browser/chat.test.mjs
@@ -33,7 +33,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const chatJS = fileURLToPath(new URL('../../internal/meet/static/chat.js', import.meta.url));
+const chatJS = fileURLToPath(new URL('../../internal/calls/static/chat.js', import.meta.url));
const src = readFileSync(chatJS, 'utf8');
// lift returns the source of one `function name(...) {...}` by matching
diff --git a/test/browser/devices.test.mjs b/test/browser/devices.test.mjs
index 6b6c4bc..dcf8873 100644
--- a/test/browser/devices.test.mjs
+++ b/test/browser/devices.test.mjs
@@ -20,8 +20,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const devicesJS = fileURLToPath(new URL('../../internal/meet/static/devices.js', import.meta.url));
-const roomJS = fileURLToPath(new URL('../../internal/meet/static/room.js', import.meta.url));
+const devicesJS = fileURLToPath(new URL('../../internal/calls/static/devices.js', import.meta.url));
+const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
function lift(source, name, where) {
let start = source.indexOf('function ' + name + '(');
diff --git a/test/browser/native-auth.mjs b/test/browser/native-auth.mjs
index f1c648c..25d2624 100644
--- a/test/browser/native-auth.mjs
+++ b/test/browser/native-auth.mjs
@@ -11,7 +11,19 @@ try {
const page = await context.newPage();
await page.goto(config.url);
process.stdout.write("stage:signin\n");
- await page.locator('input[name="address"]').fill(config.address);
+ // A member's target lands on the sign-in page. An invitation now lands on
+ // the guest screen instead (guest.html, added with guest joining): join in
+ // the app, a name form to continue in the browser, and a link to sign in.
+ // Only the sign-in page carries the address field, so follow the link when
+ // that is what this target produced.
+ const address = page.locator('input[name="address"]');
+ if ((await address.count()) === 0) {
+ await Promise.all([
+ page.waitForURL('**/signin*'),
+ page.getByRole('link', { name: 'sign in', exact: true }).click(),
+ ]);
+ }
+ await address.fill(config.address);
await Promise.all([
page.waitForURL('**/signin?sent=1'),
page.locator('form[action="/signin"] button[type="submit"]').click(),
diff --git a/test/browser/negotiation.test.mjs b/test/browser/negotiation.test.mjs
index 5097f5f..79a7aec 100644
--- a/test/browser/negotiation.test.mjs
+++ b/test/browser/negotiation.test.mjs
@@ -25,7 +25,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const roomJS = fileURLToPath(new URL('../../internal/meet/static/room.js', import.meta.url));
+const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
// lift returns the source of one top-level-ish `function name(...) {...}`
// by matching braces from its declaration.
diff --git a/test/browser/room-lane.test.mjs b/test/browser/room-lane.test.mjs
index 68dd7fe..345b452 100644
--- a/test/browser/room-lane.test.mjs
+++ b/test/browser/room-lane.test.mjs
@@ -21,7 +21,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const roomJS = fileURLToPath(new URL('../../internal/meet/static/room.js', import.meta.url));
+const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
const src = readFileSync(roomJS, 'utf8');
function lift(source, name) {
diff --git a/test/browser/room-share.test.mjs b/test/browser/room-share.test.mjs
index 9b257db..aa20f7a 100644
--- a/test/browser/room-share.test.mjs
+++ b/test/browser/room-share.test.mjs
@@ -30,7 +30,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-const roomJS = fileURLToPath(new URL('../../internal/meet/static/room.js', import.meta.url));
+const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
const src = readFileSync(roomJS, 'utf8');
// lift returns the source of one `function name(...) {...}` by matching