Compare commits
2
Commits
718f4a99f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9429dacac5 | ||
|
|
1ce72e7ac3 |
@@ -249,8 +249,8 @@ jobs:
|
||||
run: |
|
||||
$projectRoot = $PWD.ProviderPath
|
||||
$electronBuilderWorkspace = Join-Path $env:TEMP ([guid]::NewGuid().ToString('N'))
|
||||
$electronBuilderCache = Join-Path $electronBuilderWorkspace 'electron-builder-cache'
|
||||
$electronCache = Join-Path $electronBuilderWorkspace 'electron-cache'
|
||||
$electronBuilderCache = Join-Path $env:LOCALAPPDATA 'electron-builder\Cache'
|
||||
$electronCache = Join-Path $env:LOCALAPPDATA 'electron\Cache'
|
||||
$locationPushed = $false
|
||||
|
||||
function Invoke-RoboCopy {
|
||||
@@ -266,21 +266,68 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-WinCodeSignCache {
|
||||
param(
|
||||
[string]$CacheRoot,
|
||||
[string]$SevenZip
|
||||
)
|
||||
|
||||
# electron-builder downloads winCodeSign for rcedit (icon and version stamping)
|
||||
# even when nothing is signed. Its two darwin symlinks cannot be recreated
|
||||
# without SeCreateSymbolicLinkPrivilege, so seed the cache without them.
|
||||
# The version must match the one app-builder resolves, otherwise it downloads
|
||||
# its own copy and fails on the symlinks again.
|
||||
$version = 'winCodeSign-2.6.0'
|
||||
$target = Join-Path $CacheRoot "winCodeSign\$version"
|
||||
|
||||
if (Test-Path $target) {
|
||||
return
|
||||
}
|
||||
|
||||
$staging = "$target.incomplete"
|
||||
$archive = Join-Path $env:TEMP "$version.7z"
|
||||
$url = "https://github.com/electron-userland/electron-builder-binaries/releases/download/$version/$version.7z"
|
||||
|
||||
Remove-Item $staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
Invoke-WebRequest -Uri $url -OutFile $archive -UseBasicParsing
|
||||
& $SevenZip x -bd -y "-o$staging" '-x!darwin' $archive | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "failed to extract $version with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null
|
||||
Move-Item $staging $target
|
||||
} finally {
|
||||
Remove-Item $archive -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Stage the packaging inputs into a real short-path directory.
|
||||
# electron-builder rejects junction-backed files during asar creation
|
||||
# because their resolved path sits outside the package root.
|
||||
New-Item -ItemType Directory -Path $electronBuilderWorkspace | Out-Null
|
||||
New-Item -ItemType Directory -Path $electronBuilderCache | Out-Null
|
||||
New-Item -ItemType Directory -Path $electronCache | Out-Null
|
||||
New-Item -ItemType Directory -Path $electronBuilderCache -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $electronCache -Force | Out-Null
|
||||
$env:ELECTRON_BUILDER_CACHE = $electronBuilderCache
|
||||
$env:ELECTRON_CACHE = $electronCache
|
||||
|
||||
try {
|
||||
Initialize-WinCodeSignCache `
|
||||
-CacheRoot $electronBuilderCache `
|
||||
-SevenZip (Join-Path $projectRoot 'node_modules\7zip-bin\win\x64\7za.exe')
|
||||
|
||||
Copy-Item -Path (Join-Path $projectRoot 'package.json') -Destination (Join-Path $electronBuilderWorkspace 'package.json') -Force
|
||||
Copy-Item -Path (Join-Path $projectRoot 'package-lock.json') -Destination (Join-Path $electronBuilderWorkspace 'package-lock.json') -Force
|
||||
Invoke-RoboCopy (Join-Path $projectRoot 'dist') (Join-Path $electronBuilderWorkspace 'dist')
|
||||
Invoke-RoboCopy (Join-Path $projectRoot 'docs-site/build') (Join-Path $electronBuilderWorkspace 'docs-site/build')
|
||||
Invoke-RoboCopy (Join-Path $projectRoot 'images') (Join-Path $electronBuilderWorkspace 'images')
|
||||
Invoke-RoboCopy (Join-Path $projectRoot 'tools') (Join-Path $electronBuilderWorkspace 'tools')
|
||||
Invoke-RoboCopy (Join-Path $projectRoot 'node_modules') (Join-Path $electronBuilderWorkspace 'node_modules')
|
||||
|
||||
Push-Location $electronBuilderWorkspace
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Emergency Fix Pack — MetoYou / Toju
|
||||
|
||||
> **Purpose:** Give other agents a single, user-first map of what this app is, how critical features are supposed to work, where the code lies, and the exact order to fix failures.
|
||||
>
|
||||
> **Constraint for this pack:** Analysis and planning only. Product code was not changed when this pack was authored (2026-08-12).
|
||||
>
|
||||
> **Primary surface:** `toju-app/` + targeted `electron/` (auth secret store / IPC). Expand to `server/` / `e2e/` only when a packet says so.
|
||||
|
||||
---
|
||||
|
||||
## How to use this pack
|
||||
|
||||
1. Read **this file** + `01-product-overview.md`.
|
||||
2. Pick a work packet from `12-agent-work-packets.md` (do not invent parallel scope).
|
||||
3. Open the matching issue file (`04`–`09`) for user story, intended behavior, failure modes, code paths, and proof.
|
||||
4. Follow priority order in `11-fix-priority-plan.md` unless the user overrides.
|
||||
5. Before coding: run the repo **interview-before-implement** ritual (choices A/B/C) unless the user says “just fix it” / “no interview” or an active handoff already records approved decisions.
|
||||
6. When a packet finishes: update `11-fix-priority-plan.md` status checkboxes and clear or rewrite `agents-docs/HANDOFF.md` per `.cursor/rules/handoff.mdc`.
|
||||
|
||||
---
|
||||
|
||||
## File index
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `00-README.md` | This index and rules of engagement |
|
||||
| `01-product-overview.md` | What MetoYou/Toju is (user + architecture) |
|
||||
| `02-user-journeys.md` | End-to-end journeys: login → rooms → chat → voice → multi-server |
|
||||
| `03-architecture-map.md` | Domains, transports, identity model, key paths |
|
||||
| `04-auth-login-bugs.md` | Login / authorize / silent provision failures |
|
||||
| `05-signaling-multi-server.md` | Signal servers, WS drops, room affinity, presence |
|
||||
| `06-voice-webrtc-bugs.md` | Voice, camera, screen-share, peer negotiation |
|
||||
| `07-data-channel-drops.md` | Control-plane DC failures and recovery gaps |
|
||||
| `08-messaging-visibility.md` | “Messages not seen” / sync / fallback |
|
||||
| `09-identity-cross-signal.md` | Home vs foreign actor ids (calls, DMs, voice routing) |
|
||||
| `10-code-lies-doc-debt.md` | Docs/lessons that disagree with the tree |
|
||||
| `11-fix-priority-plan.md` | Ordered waves, acceptance, proof |
|
||||
| `12-agent-work-packets.md` | Copy-paste packets for new chats |
|
||||
| `13-validated-findings-fable-5-handoff.md` | Code-validated findings, corrected priorities, E2E limits, and implementation handoff |
|
||||
| `e2e-failures/` | Latest full Playwright e2e run baseline (errors + slow tests) |
|
||||
|
||||
---
|
||||
|
||||
## Product in one paragraph (user view)
|
||||
|
||||
MetoYou (product client **Toju**) is a **desktop-first P2P chat app**: you log in once to a **signal server**, join **chat-servers** (communities with text + voice channels), talk in text, join voice, share screen/camera, and DM / call people. Media and most chat go **peer-to-peer over WebRTC**. The signal server only authenticates you, tracks who is in which room, and relays WebRTC offers / narrow chat & DM fallbacks. You may use **many signal servers**; the app is supposed to create a linked account on each new host **silently** after the first home login.
|
||||
|
||||
---
|
||||
|
||||
## Critical bug themes (user language)
|
||||
|
||||
1. **“It keeps asking me to log in”** even though I’m already signed in (especially when joining something on another signal server).
|
||||
2. **“Voice doesn’t work / one-way / connecting forever”** — often after reconnect or when people registered on different signal servers.
|
||||
3. **“Connection / data channel drops”** — chat, attachments, emoji, and sometimes voice die; recovery is silent or tears everything down.
|
||||
4. **“I sent a message but they don’t see it”** (or only some devices see it) — presence missing, P2P sync dead, or wrong identity.
|
||||
5. **“Call rings nobody / I’m In Voice alone”** — cross-signal identity mismatch on outbound or inbound paths.
|
||||
6. **Different signal servers** make all of the above worse because **home user id ≠ foreign provisioned actor id**, and signaling is **not federated**.
|
||||
|
||||
---
|
||||
|
||||
## Related existing research (do not redo)
|
||||
|
||||
- Active research story: `agents-docs/user-stories/silent-cross-signal-server-auth.md`
|
||||
- Feature contracts: `agents-docs/features/{authentication,signaling,voice-webrtc,messaging,server-directory}.md`
|
||||
- Realtime deep dive: `toju-app/src/app/infrastructure/realtime/README.md`
|
||||
- Lessons index: `agents-docs/LESSONS-INDEX.md` — tags `[auth] [realtime] [direct-call] [direct-message] [identity] [signaling]`
|
||||
|
||||
---
|
||||
|
||||
## Non-goals of this pack
|
||||
|
||||
- Implementing fixes (separate chats / packets).
|
||||
- Rewriting the whole monorepo.
|
||||
- Treating unit-green alone as “done” — each packet lists user-visible proof.
|
||||
@@ -1,118 +0,0 @@
|
||||
# 01 — Product overview (user + system)
|
||||
|
||||
## What the user thinks this app is
|
||||
|
||||
**Toju / MetoYou** is a Discord-like desktop chat app with:
|
||||
|
||||
- **Accounts** on a signal (signaling) server
|
||||
- **Chat-servers** (communities) with text channels and voice channels
|
||||
- **Live text chat** in those channels
|
||||
- **Voice / camera / screen share** in voice channels
|
||||
- **Direct messages** and **private calls**
|
||||
- **Friends**, profile cards, custom emoji, GIFs, file attachments
|
||||
- **Multiple signal servers** in the network settings (home + others)
|
||||
|
||||
The marketing promise implied by the product design:
|
||||
|
||||
> Log in once. Join communities anywhere on the network. Chat and voice “just work.” Switching signal hosts should not feel like logging into a second product.
|
||||
|
||||
---
|
||||
|
||||
## What the system actually is
|
||||
|
||||
| Layer | Role |
|
||||
|-------|------|
|
||||
| **Angular client** (`toju-app/`) | All UX, NgRx state, domain logic, WebRTC + WebSocket clients |
|
||||
| **Electron shell** (`electron/`) | Desktop window, SQLite persistence, IPC (`window.api`), provision-secret safeStorage, screen capture helpers |
|
||||
| **Signaling server** (`server/`) | Auth tokens, public server directory REST, WebSocket identify/join/presence, RTC offer/answer/ICE relay, narrow chat/DM/voice_state fallbacks, multi-device `account_sync`. **Does not store chat history.** |
|
||||
| **Web / Capacitor** | Same Angular app; weaker secret storage (sessionStorage); mobile voice/UI constraints |
|
||||
|
||||
### Transport split (critical mental model)
|
||||
|
||||
| Transport | What the user experiences | What it actually carries |
|
||||
|-----------|---------------------------|--------------------------|
|
||||
| **WebSocket to signal server** | “I’m online / in this server / someone joined voice” | Identity, room membership, presence, SDP/ICE relay, `chat_message` + DM fallbacks, `voice_state`, `account_sync` |
|
||||
| **WebRTC media** | Hearing / seeing people | Mic, camera, screen tracks (never through the signal server) |
|
||||
| **WebRTC ordered data channel** | Messages syncing, files, emoji, many “live” features | Chat events, inventory sync, attachments, avatar/emoji chunks, voice/screen control, plugin bus |
|
||||
|
||||
If the **socket** is wrong or unauthenticated → user is invisible; no relay; chat fallback may fail.
|
||||
If the **data channel** is dead → live text may still limp via `chat_message`, but **history sync, attachments, emoji, many controls** fail.
|
||||
If **media** is routed wrong or peer never connects → voice UI lies (“In Voice”) with silence.
|
||||
|
||||
---
|
||||
|
||||
## Core vocabulary (user ↔ engineering)
|
||||
|
||||
| User says | Engineering term |
|
||||
|-----------|------------------|
|
||||
| “My account / login” | Home session on `homeSignalServerUrl` + local profile in SQLite/IndexedDB |
|
||||
| “Another network / signal host” | Foreign `ServerEndpoint` URL; needs per-URL credential |
|
||||
| “A server / community” | Saved **Room** (chat-server) with `sourceUrl` / `sourceId` = which signal hosts it |
|
||||
| “Text channel / voice channel” | Channels inside a room; text vs voice types |
|
||||
| “I’m in voice” | Local `VoiceSession` + `voice_state` with `isConnected` + mic ownership via `clientInstanceId` |
|
||||
| “DM / call someone” | Direct-message conversation + optional `direct-call` session |
|
||||
| “It logged me out” | Often `/login` or `/login?mode=authorize` — may be **foreign authorize**, not true home logout |
|
||||
|
||||
---
|
||||
|
||||
## Multi-signal identity (the footgun)
|
||||
|
||||
1. User registers on **Signal A** → home user id `H`.
|
||||
2. User later needs **Signal B** → client is supposed to **auto-register/login** with a local **provision secret**, creating actor id `A_B` on B (username may be suffixed).
|
||||
3. Peers on Signal B see the user as `A_B`, not `H`.
|
||||
4. WebRTC peer map keys, `targetUserId` on relay, DM conversation ids, and call participant lists may mix `H` and `A_B`.
|
||||
|
||||
**Invariant the product claims:** one human, one local profile, many per-server actor credentials — UX never asks for password again except last-resort authorize / real home expiry.
|
||||
|
||||
**Invariant signaling claims:** non-federated — peers in the same room must share the **same signal endpoint** to discover each other. Cross-room “same person” is a client-side identity problem.
|
||||
|
||||
---
|
||||
|
||||
## Feature inventory (what “done product” includes)
|
||||
|
||||
Must work for a release-quality emergency fix:
|
||||
|
||||
1. Home register / login / logout / session restore
|
||||
2. Silent foreign provision + Network settings badges
|
||||
3. Discover / create / join / leave chat-servers (public, password, invite, moderated)
|
||||
4. Text channels: send, edit, delete, react, typing, unread, sync after late join
|
||||
5. Voice channels: join/leave, mute/deafen, speaking indicators, multi-device takeover, move between channels
|
||||
6. Camera + screen share in voice / calls
|
||||
7. DMs + friends + delivery states
|
||||
8. Direct / group calls (ring, answer, decline, DND)
|
||||
9. Attachments over data channel
|
||||
10. Multi-device `account_sync` for saved rooms / chat batches / friends / avatar / emoji
|
||||
11. Endpoint health, version compatibility, room signal affinity + fallback
|
||||
|
||||
Secondary (do not block P0 voice/auth/chat): plugins, game activity, themes, KLIPY, link previews, experimental media.
|
||||
|
||||
---
|
||||
|
||||
## Platforms
|
||||
|
||||
| Platform | Notes for bugs |
|
||||
|----------|----------------|
|
||||
| Electron desktop | Primary; provision secret in safeStorage; SQLite; best screen share |
|
||||
| Browser | sessionStorage provision secret dies with tab; IndexedDB |
|
||||
| Capacitor mobile | Auth routing, mic permissions, no reliable screen share; title bar hidden |
|
||||
|
||||
---
|
||||
|
||||
## Trust boundaries (short)
|
||||
|
||||
- REST mutations and WS `identify` require bearer/session token **per signal URL**.
|
||||
- Actor user ids in request bodies are ignored server-side; token wins.
|
||||
- Message bodies are **not E2E encrypted** beyond DTLS on WebRTC and TLS on WS.
|
||||
- Signaling server is **not** the source of truth for chat history.
|
||||
|
||||
---
|
||||
|
||||
## Where truth lives (for agents)
|
||||
|
||||
| Kind of truth | Prefer |
|
||||
|---------------|--------|
|
||||
| Wire WebSocket types | `agents-docs/features/signaling.md` + `server/src/websocket/handler.ts` — **not** `shared-kernel/signaling-contracts.ts` |
|
||||
| Auth multi-server | `agents-docs/features/authentication.md` + `emergency-fix/04-*.md` |
|
||||
| Voice/WebRTC plumbing | `toju-app/.../realtime/README.md` — verify against code (see `10-code-lies`) |
|
||||
| Domain UX | `toju-app/src/app/domains/<name>/README.md` |
|
||||
| Known past bugs | `agents-docs/LESSONS.md` — verify symbols still exist (some lessons describe **desired** fixes as if shipped) |
|
||||
@@ -1,256 +0,0 @@
|
||||
# 02 — User journeys (how it is supposed to feel)
|
||||
|
||||
Each journey: **steps the user takes**, **what should happen**, **failure symptoms**, **where to dig**.
|
||||
|
||||
---
|
||||
|
||||
## J1 — First-time home login
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Open app → login/register.
|
||||
2. Pick (or accept default) signal server.
|
||||
3. Register or log in with username/password.
|
||||
4. Land on dashboard / saved servers.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Session token stored for that URL (`metoyou.authTokens` + credential store).
|
||||
- Local user profile scoped in DB; NgRx `currentUser` set with `homeSignalServerUrl`.
|
||||
- **Provision secret** created and persisted (Electron safeStorage / web sessionStorage).
|
||||
- Signing public key registered on **home** server when possible.
|
||||
- WebSocket connects, `identify` with home token + `clientInstanceId`, ready for joins.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Stuck on login; bounce back after “success”.
|
||||
- Dashboard with no token → later `SESSION_EXPIRED`.
|
||||
- Later foreign joins always open authorize (missing secret).
|
||||
|
||||
**Code**
|
||||
|
||||
- `domains/authentication/` (`AuthenticationService`, login/register UI)
|
||||
- `store/users/users.effects.ts` (`authenticateUser`, `prepareAuthenticatedUserStorage`, `ensureHomeProvisionSecret`)
|
||||
- Feature: `agents-docs/features/authentication.md`
|
||||
|
||||
---
|
||||
|
||||
## J2 — Restart app still logged in
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Quit Electron fully; reopen.
|
||||
2. Expect same user without typing password.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Load user from local DB + valid home token (credential store or legacy token fallback).
|
||||
- Identify on home (and later foreign) sockets.
|
||||
- Opportunistic `ensureProvisioned` for active endpoints **without** login UI.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Flash of dashboard then `/login`.
|
||||
- Profile restored but chat/presence dead (“alone”).
|
||||
- Foreign rooms immediately open `/login?mode=authorize`.
|
||||
|
||||
**Code**
|
||||
|
||||
- `loadCurrentUser$`, `hasValidPersistedSession`, `migrateHomeCredential`
|
||||
- Lessons: identify legacy token fallback; persisted user still needs token
|
||||
|
||||
---
|
||||
|
||||
## J3 — Join a community on the *same* signal server
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Discover or invite → Join.
|
||||
2. Open a text channel; send “hello”.
|
||||
3. Join a voice channel; talk.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- REST join with bearer; then WS `join_server`.
|
||||
- Receive `server_users` / `user_joined`; peer mesh forms.
|
||||
- Text: local add + DC `chat-message` + WS `chat_message` fallback.
|
||||
- Voice: `voice_state` broadcast; WebRTC offer/answer; same-channel audio routing.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Joined in UI but not in others’ member lists (identify/join race).
|
||||
- Messages only on sender device.
|
||||
- Voice tile appears, no audio.
|
||||
|
||||
---
|
||||
|
||||
## J4 — Join a community on a *different* signal server (critical)
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Already logged into home Signal A.
|
||||
2. Open invite / browser card whose `sourceUrl` is Signal B.
|
||||
3. Join and chat/voice.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
1. `ensureCredentialForServerUrl(B)` → silent provision (or reuse credential).
|
||||
2. REST + WS use **actor id on B**, not home id.
|
||||
3. No authorize login page.
|
||||
4. Peers on B see actor display name (may show `#prefix` disambiguation).
|
||||
5. Voice/chat use B’s WebSocket for that room’s affinity.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Redirect to `/login?mode=authorize&serverId=…` while user bar still shows logged in.
|
||||
- Join “succeeds” locally but invisible on B.
|
||||
- Can see members but WebRTC never connects (initiator/identity mismatch).
|
||||
- DMs/calls to that person later miss rings or fork conversations.
|
||||
|
||||
**Code**
|
||||
|
||||
- `SignalServerAuthorizeService`, `SignalServerProvisionerService`, `room-signaling-connection.ts`
|
||||
- Story: `agents-docs/user-stories/silent-cross-signal-server-auth.md`
|
||||
- Pack: `04-auth-login-bugs.md`, `09-identity-cross-signal.md`
|
||||
|
||||
---
|
||||
|
||||
## J5 — Send a text message (server channel)
|
||||
|
||||
**User steps**
|
||||
|
||||
1. In text channel, type and send.
|
||||
2. Peer in same room should see it live; late joiner should catch up after connecting.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Optimistic local message with stable id (attachments bind to that id).
|
||||
- Broadcast on data channel; also relay `chat_message` on signaling for peers without DC.
|
||||
- Edits/deletes primarily P2P (+ `account_sync` to sibling devices).
|
||||
- On peer connect: inventory ↔ sync-batch (up to 20k recent msgs, chunks of 200).
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Sender sees it; others don’t (no presence / no join / DC+fallback both fail).
|
||||
- Others see live but not history (DC inventory never ran).
|
||||
- Multi-device: one device has history, another empty (`account_sync` / identify).
|
||||
- Attachments “Waiting for image…” forever (announce vs message ordering).
|
||||
|
||||
**Pack:** `08-messaging-visibility.md`
|
||||
|
||||
---
|
||||
|
||||
## J6 — Join voice in a channel
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Click a voice channel.
|
||||
2. Grant mic if prompted.
|
||||
3. Hear others; they hear you; speaking indicators; optional camera/screen.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Leave any previous voice/call first (exclusive).
|
||||
- Publish `voice_state` with channel/server ids + `clientInstanceId`.
|
||||
- Only one device owns mic (others passive; Join = takeover).
|
||||
- Peer connections already for chat mesh; mic tracks attached only to same-channel peers.
|
||||
- Playback only for peers in same voice channel.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- “Connecting” forever.
|
||||
- One-way audio.
|
||||
- UI shows peers in channel but silent.
|
||||
- After network blip: forever dead until full app restart.
|
||||
- Works same-home, fails cross-home (initiator uses home id vs peer actor id).
|
||||
|
||||
**Pack:** `06-voice-webrtc-bugs.md`
|
||||
|
||||
---
|
||||
|
||||
## J7 — Call someone from DM / people card
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Open DM or people card → Call.
|
||||
2. Callee hears ring / sees modal (unless DND).
|
||||
3. Answer → private call UI with optional chat panel.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- `direct-call` event delivered via PeerDelivery (DC then signaling).
|
||||
- `targetUserId` = callee’s **currently connected signal identity**.
|
||||
- Callee admission checks **all local aliases** (home + every provisioned actor id).
|
||||
- Caller never sits “In Voice” if ring could not be delivered.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Caller In Voice; callee silent (outbound route null / wrong id).
|
||||
- Callee never notified (inbound alias filter — partially fixed).
|
||||
- Cross-signal: two DM threads; replies land in the “wrong” empty one.
|
||||
|
||||
**Pack:** `09-identity-cross-signal.md`
|
||||
|
||||
---
|
||||
|
||||
## J8 — Network blip / signal server restart
|
||||
|
||||
**User steps**
|
||||
|
||||
1. In voice + chat; Wi‑Fi blips or signal process restarts.
|
||||
2. Continue without manual reconnect.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- WS reconnect with backoff; health probe forces fresh socket on instance change.
|
||||
- `reIdentifyAndRejoin` then room resync.
|
||||
- Peer disconnect grace 10s; then reconnect loop (~12 × 5s).
|
||||
- DC close triggers repair; chat fallback covers live text meanwhile.
|
||||
- Voice presence clears on dead voice-active disconnect server-side.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Zombie “online” with no events.
|
||||
- Give-up after ~60s with **no error UI**.
|
||||
- Docs promise soft DC replace; code tears down full peer (audio drop).
|
||||
- Identify skipped → alone forever until manual leave/rejoin.
|
||||
|
||||
**Pack:** `05-signaling-multi-server.md`, `07-data-channel-drops.md`
|
||||
|
||||
---
|
||||
|
||||
## J9 — Two devices, same account
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Desktop in voice; phone/browser also logged in.
|
||||
2. Second device shows “in voice on another device”; can Takeover.
|
||||
3. Chat/history appears on both.
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Distinct `clientInstanceId` per tab (sessionStorage).
|
||||
- Broadcasts reach sibling connections; `account_sync` for owned state.
|
||||
- Voice exclusive; takeover yields mic on old owner.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Tabs evict each other (shared clientInstanceId in localStorage — lesson says use sessionStorage).
|
||||
- Second device never gets chat batches.
|
||||
- Both think they own voice / neither transmits.
|
||||
|
||||
---
|
||||
|
||||
## J10 — Logout
|
||||
|
||||
**User steps**
|
||||
|
||||
1. Title-bar Logout (desktop) or Settings → Logout (mobile).
|
||||
|
||||
**Supposed to happen**
|
||||
|
||||
- Disconnect sockets; clear current user id; reset rooms/users/messages; `/login`.
|
||||
|
||||
**Broken looks like**
|
||||
|
||||
- Stale credentials for foreign URLs linger and confuse next account (verify credential clear scope when fixing auth).
|
||||
@@ -1,154 +0,0 @@
|
||||
# 03 — Architecture map (for agents)
|
||||
|
||||
## Bounded contexts (product client)
|
||||
|
||||
See `toju-app/src/app/domains/README.md`. Emergency-relevant domains:
|
||||
|
||||
| Domain | Owns |
|
||||
|--------|------|
|
||||
| `authentication` | Login/register HTTP, provision secret, per-URL credentials, authorize navigation |
|
||||
| `server-directory` | Endpoints, health, discovery, invites, room metadata affinity |
|
||||
| `chat` | Message rules, sync rules, chat UI |
|
||||
| `direct-message` | DMs, friends, offline queue, PeerDelivery usage |
|
||||
| `direct-call` | Private call sessions / rings |
|
||||
| `voice-session` | Join/leave bookkeeping, floating controls, settings storage |
|
||||
| `voice-connection` | Facade over realtime for mic/camera/playback/VAD |
|
||||
| `screen-share` | Picker / quality |
|
||||
| `attachment` | Chunked P2P files |
|
||||
| `access-control` | Permissions / bans |
|
||||
|
||||
**Infrastructure (not a domain):** `infrastructure/realtime/` (WebRTC + signaling), `infrastructure/persistence/`, `infrastructure/mobile/`.
|
||||
|
||||
**Global NgRx:** `store/users`, `store/rooms`, `store/messages` — orchestration across domains.
|
||||
|
||||
---
|
||||
|
||||
## Identity & credentials
|
||||
|
||||
```
|
||||
Home profile (NgRx User)
|
||||
id / username / displayName / homeSignalServerUrl
|
||||
│
|
||||
├─ AuthTokenStore (legacy per-URL token) metoyou.authTokens
|
||||
├─ SignalServerCredentialStore metoyou.signalServerCredentials
|
||||
│ { serverUrl → userId, token, provisioned? }
|
||||
└─ ProvisionSecretStore (per home user id)
|
||||
Electron safeStorage | web sessionStorage
|
||||
```
|
||||
|
||||
**Actor resolution for a room:** `SignalServerAuthService.resolveActorUserIdForServer(sourceUrl, homeOderId)` — foreign URL must use provisioned `userId`.
|
||||
|
||||
**Identify on socket:** `SignalingTransportHandler.getIdentifyCredentialsForSignalUrl(url)` must resolve token+actor for that URL (store fallback).
|
||||
**Danger:** `getIdentifyCredentials()` returns **home** credential — used for some localOderId / polite-peer paths → cross-signal initiator bugs (`06`, `09`).
|
||||
|
||||
---
|
||||
|
||||
## Room ↔ signal affinity
|
||||
|
||||
```
|
||||
Room { id, channels[], sourceUrl, sourceId, ... }
|
||||
│
|
||||
▼
|
||||
RoomSignalingConnection
|
||||
ensureCredentialForServerUrl(sourceUrl)
|
||||
identify(actor for sourceUrl)
|
||||
join_server / view_server on that WS
|
||||
│
|
||||
▼
|
||||
ServerSignalingCoordinator
|
||||
maps serverId ↔ signalUrl ↔ peers
|
||||
```
|
||||
|
||||
Fallback: try other online compatible endpoints on outage; **do not** treat Cloudflare 521/522 as “client incompatible”. Non-federated: peers must converge on same endpoint for that room.
|
||||
|
||||
---
|
||||
|
||||
## Realtime composition root
|
||||
|
||||
`WebRTCService` (`realtime-session.service.ts`) wires:
|
||||
|
||||
- `SignalingTransportHandler` → many `SignalingManager` (one WS per URL)
|
||||
- `PeerConnectionManager` → negotiation, DC, recovery
|
||||
- `MediaManager` / noise / screen share
|
||||
- `WebRtcStateController` (signals)
|
||||
|
||||
Inbound WS → `SignalingMessageHandler` → users/rooms/voice/chat effects.
|
||||
|
||||
---
|
||||
|
||||
## Message send path (server channel)
|
||||
|
||||
```
|
||||
Composer → MessagesActions.sendMessage({ id? })
|
||||
→ local DB + NgRx
|
||||
→ DC broadcast chat-message (+ message-revision)
|
||||
→ WS chat_message fallback (room members)
|
||||
→ account_sync to sibling devices
|
||||
```
|
||||
|
||||
Receive gates: room must be current or saved; channel scoping for text.
|
||||
|
||||
---
|
||||
|
||||
## Voice join path
|
||||
|
||||
```
|
||||
UI join channel
|
||||
→ VoiceSessionFacade startSession
|
||||
→ leave previous exclusive target
|
||||
→ enableVoice / heartbeat
|
||||
→ voice_state on WS (+ DC control)
|
||||
→ MediaManager.syncVoiceRouting (same channel only)
|
||||
→ VoicePlaybackService for remote same-channel peers
|
||||
```
|
||||
|
||||
Peer PC may already exist from presence mesh; join mainly attaches tracks + announces state.
|
||||
|
||||
---
|
||||
|
||||
## Call / DM delivery path
|
||||
|
||||
```
|
||||
DirectCallService / DirectMessageService
|
||||
→ PeerDeliveryService
|
||||
1) data channel if open
|
||||
2) signaling forward (targetUserId)
|
||||
3) offline queue (DM) / silent fail (call if ignored)
|
||||
```
|
||||
|
||||
Inbound call admission: `direct-call-participant-identity.rules.ts` (aliases — implemented).
|
||||
Outbound routable id pick: **documented in LESSONS as fixed; symbols not in tree** — see `10-code-lies`.
|
||||
|
||||
---
|
||||
|
||||
## Recovery constants (cheat sheet)
|
||||
|
||||
| Constant | Value |
|
||||
|----------|-------|
|
||||
| WS reconnect backoff | 1s → 30s |
|
||||
| Connect timeout | 5s |
|
||||
| Keepalive interval / ack timeout | 25s / 10s |
|
||||
| Health probe | 5s |
|
||||
| Peer disconnect grace | 10s |
|
||||
| Peer reconnect | 5s × 12 then silent abandon |
|
||||
| DC recovery grace (closing) | 2.5s |
|
||||
| Non-initiator give-up | 5s |
|
||||
| Offer-sent grace | 20s |
|
||||
| Inventory / full sync limit | 20_000 |
|
||||
| Sync batch | 200 |
|
||||
| Sync poll | 10s fast / 15 min slow |
|
||||
|
||||
Source: `realtime.constants.ts`, `message-sync.rules.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Server responsibilities (only when needed)
|
||||
|
||||
Default agent scope excludes `server/` unless packet expands. Know this:
|
||||
|
||||
- Serializes WS handlers per connection (identify-before-join).
|
||||
- Relays RTC only when peers share membership (DM types exempt).
|
||||
- No message persistence.
|
||||
- `voiceActive` routes offers to owning connection.
|
||||
|
||||
Canonical envelopes: `agents-docs/features/signaling.md`.
|
||||
@@ -1,119 +0,0 @@
|
||||
# 04 — Auth & login bugs
|
||||
|
||||
> **User theme:** “I’m logged in but it keeps asking me to log in / authorize.”
|
||||
> **Severity:** P0 — blocks multi-server chat, voice, presence.
|
||||
> **Existing research:** `agents-docs/user-stories/silent-cross-signal-server-auth.md` (do not redo; implement after interview).
|
||||
|
||||
---
|
||||
|
||||
## How auth is supposed to work (user view)
|
||||
|
||||
1. Register or log in **once** on a home signal server.
|
||||
2. Stay signed in across app restarts (desktop).
|
||||
3. When touching another signal server (join, invite, create room, activate endpoint): the app **silently** creates or reuses an account there.
|
||||
4. You only see a login form again if:
|
||||
- true home session expired / missing token, or
|
||||
- last-resort: username collision exhaustion / user clicked Sign in in Network settings.
|
||||
5. Offline / dead endpoints never bounce you to authorize login.
|
||||
|
||||
Settings → Network may show `Authorized` / `Needs sign-in` as diagnostics — not as the default path for normal joins.
|
||||
|
||||
---
|
||||
|
||||
## How auth is supposed to work (system)
|
||||
|
||||
| Concept | Role |
|
||||
|---------|------|
|
||||
| Home credential | Token + user id for `homeSignalServerUrl` |
|
||||
| Foreign credential | Separate user id + token per URL in `SignalServerCredentialStore` |
|
||||
| Provision secret | Password used only for auto register/login on foreign hosts |
|
||||
| `ensureProvisioned` | Register-or-login with secret; suffix username on collision |
|
||||
| `ensureCredentialForServerUrl` | Gate before foreign room connect; navigate authorize only for `collision` or `no-provision-secret` when endpoint online |
|
||||
| `authorize` mode | Manual login that upserts foreign credential **without** resetting home profile |
|
||||
| `auth_required` vs `auth_error` | Race vs rejected token — must not falsely expire home on foreign races |
|
||||
|
||||
Authorize nav rule: `shouldNavigateToAuthorizeSignalServer` in `signal-server-authorize.rules.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes (code-backed)
|
||||
|
||||
### A — Missing provision secret → authorize UI (primary)
|
||||
|
||||
**Symptom:** Logged-in user opens foreign room/invite → `/login?mode=authorize`.
|
||||
|
||||
**Mechanism**
|
||||
|
||||
1. `ensureProvisioned` → `{ kind: 'skipped', reason: 'no-provision-secret' }`.
|
||||
2. `ensureCredentialForServerUrl` navigates to authorize.
|
||||
3. Authorize mode does not auto-leave when `currentUser` exists → stuck prompt.
|
||||
|
||||
**Why secret missing**
|
||||
|
||||
- Created only in `prepareAuthenticatedUserStorage` when `homeSignalServerUrl` + `loginResponse` present.
|
||||
- Session restore calls `ensureProvisioned` but **does not** ensure secret exists first.
|
||||
- Web: sessionStorage secret dies with tab.
|
||||
- Old installs / wiped Electron `userData/provision-secrets/`.
|
||||
|
||||
**Files**
|
||||
|
||||
- `signal-server-auth.service.ts`, `signal-server-authorize.service.ts`
|
||||
- `provision-secret-store.service.ts`, `electron/api/provision-secret-store.ts`
|
||||
- `users.effects.ts`
|
||||
|
||||
### B — Username collision exhaustion
|
||||
|
||||
All register candidates 409 + login 401 → `collision` → authorize. Rare but real on crowded foreign servers.
|
||||
|
||||
### C — False home session expiry
|
||||
|
||||
`signalServerAuthFailed$` may `SESSION_EXPIRED` → full `/login` when home classification / retry budget wrong. Distinguish `auth_required` (re-identify) vs `auth_error` (clear credential; foreign re-provision vs home expire).
|
||||
|
||||
### D — Credential missing → unauthenticated join → invisible user
|
||||
|
||||
Without token, socket never identifies; `join_server` dropped; user alone; later gates open authorize. Cascades into “messages not seen” and “voice empty”.
|
||||
|
||||
### E — Opportunistic provision swallows errors
|
||||
|
||||
`ensureProvisioned(...).catch(() => undefined)` on health/startup → failure deferred until mid-join authorize popup.
|
||||
|
||||
### F — Others?
|
||||
---
|
||||
|
||||
## Doc / README lies in this area
|
||||
|
||||
| Claim | Reality |
|
||||
|-------|---------|
|
||||
| Auth domain README sequence: `POST /api/auth/login` | Real paths: `/api/users/login`, `/api/users/register` (`AuthenticationService`) |
|
||||
| Feature doc: offline must not open authorize | Code path exists; still fails open on missing secret when “online” |
|
||||
|
||||
---
|
||||
|
||||
## Fix directions (interview choices — not approved)
|
||||
|
||||
From user story (recommend **A**):
|
||||
|
||||
| Option | Idea |
|
||||
|--------|------|
|
||||
| **A** | On restore / before foreign provision, always `ensureHomeProvisionSecret`; keep authorize only for collision / manual |
|
||||
| **B** | Never navigate on `no-provision-secret`; toast + Network badge |
|
||||
| **C** | Durable web secret (not sessionStorage) |
|
||||
| **D** | Stronger unique usernames before collision UI |
|
||||
|
||||
---
|
||||
|
||||
## Proof of done
|
||||
|
||||
1. Two live signal servers: home register on A → join room on B → **no** `/login` navigation.
|
||||
2. Full Electron restart → foreign rejoin still silent.
|
||||
3. Offline foreign URL → no authorize navigation.
|
||||
4. Focused tests: missing secret on restore → secret created → provision → `Router.navigate(['/login'])` never called.
|
||||
5. Foreign `auth_error` with valid home → re-provision, not `SESSION_EXPIRED`.
|
||||
|
||||
---
|
||||
|
||||
## Agent scope
|
||||
|
||||
- Default: `toju-app/domains/authentication`, `store/users`, `store/rooms/room-signaling-connection.ts`, server-directory call sites.
|
||||
- Electron: provision-secret store/IPC only if persistence fix needs it.
|
||||
- Ask before deep `server/` auth changes.
|
||||
@@ -1,112 +0,0 @@
|
||||
# 05 — Signaling & multi-signal-server connection
|
||||
|
||||
> **User theme:** “I’m connected but nobody sees me / rooms flicker / different signal servers break everything.”
|
||||
> **Severity:** P0 for presence and as root cause of voice + chat visibility.
|
||||
|
||||
---
|
||||
|
||||
## How signaling is supposed to work (user view)
|
||||
|
||||
- When you open a community, you appear in the member list for everyone in that community.
|
||||
- Leaving / going offline shows you as offline (without duplicate leave spam for multi-device).
|
||||
- Switching communities updates who you see without wiping presence for other saved servers.
|
||||
- If the signal host restarts or Wi‑Fi blips, you come back automatically.
|
||||
- Communities hosted on different signal hosts still work as long as your client is authorized on that host (see `04`).
|
||||
- Users should be able to see/sync chats/files/profile images/server states info change/plugin states and events/talk without losing connectivity or only some users are seen and other aren't.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## How signaling is supposed to work (system)
|
||||
|
||||
1. **One WebSocket per signal URL** (`SignalingManager`).
|
||||
2. **Identify first** with that URL’s token + actor `oderId` + `clientInstanceId`.
|
||||
3. Then `join_server` / `view_server` for rooms on that URL.
|
||||
4. Server serializes handlers per connection so join cannot race mid-identify.
|
||||
5. Client `reIdentifyAndRejoin` on reconnect; rooms effects resync as safety net.
|
||||
6. Room traffic prefers room `sourceUrl`; temporary fallback to other compatible endpoints on outage.
|
||||
7. **Non-federated:** Signal A does not share peer registry with Signal B. Same human on two hosts = two actor ids (client glue).
|
||||
|
||||
Canonical catalog: `agents-docs/features/signaling.md`.
|
||||
Client map: `infrastructure/realtime/README.md` + `signaling/`.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
### 1 — Identify / join race or skipped identify
|
||||
|
||||
**Symptom:** Local UI shows room open; others don’t see you; you miss `user_joined` / `chat_message`; “alone”.
|
||||
|
||||
**Causes**
|
||||
|
||||
- Join sent before identify (fixed server-side serialization + client reIdentify — regressions still dangerous).
|
||||
- `getIdentifyCredentialsForSignalUrl` returns null (no credential) → no identify.
|
||||
- Identify cache empty on fresh socket without store fallback (lesson: must fall back to credential store).
|
||||
|
||||
### 2 — Wrong socket / wrong affinity
|
||||
|
||||
**Symptom:** Presence or RTC relay never reaches peers in the room; works only when everyone shares the same endpoint URL alias.
|
||||
|
||||
**Causes**
|
||||
|
||||
- Room `sourceUrl` stale vs directory `serverInstanceId` canonicalization.
|
||||
- Fallback broadcast regressions (raw room messages must not spam every manager when route unknown — README describes current intended behavior).
|
||||
- Cold start reconnect before health probes collapse aliases.
|
||||
|
||||
### 3 — Multi-signal leave tears down wrong peers
|
||||
|
||||
**Symptom:** Leaving a room on signal-sweden drops a peer still shared via signal.toju.app.
|
||||
|
||||
**Intended:** `user_left` subtracts only that cluster’s shared servers; preserve routes while P2P still live.
|
||||
|
||||
### 4 — Half-open / zombie sockets
|
||||
|
||||
**Symptom:** UI “connected”; no events after server process restart.
|
||||
|
||||
**Mitigations in tree:** keepalive ack timeout after first ack; `/api/health` probe; `serverInstanceId` change forces new WS. Regressions: skipping first heartbeat tick, treating 521/522 as incompatible.
|
||||
|
||||
### 5 — Auth failure cascade
|
||||
|
||||
Foreign `auth_required` / `auth_error` without silent re-provision → authorize UI or invisible membership (`04`).
|
||||
|
||||
### 6 — Multi-device eviction loop
|
||||
|
||||
Shared `clientInstanceId` in localStorage across tabs → server evicts sibling on identify. Must stay in **sessionStorage**.
|
||||
|
||||
---
|
||||
|
||||
## Different signal servers — user-visible contract
|
||||
|
||||
| Situation | Expected |
|
||||
|-----------|----------|
|
||||
| Two users, same room, same signal URL | Discover each other; RTC relay OK |
|
||||
| Two users, “same” community mirrored on different signals | **Not supported as one mesh** — they are different rooms unless directory/affinity converges them |
|
||||
| One user, rooms on A and B | Two sockets; two actor ids; local profile one |
|
||||
| DM/call across people who met on foreign rooms | Must address **connected** actor id (see `09`) |
|
||||
|
||||
---
|
||||
|
||||
## Key files
|
||||
|
||||
- `signaling.manager.ts`, `signaling-transport-handler.ts`, `server-signaling-coordinator.ts`
|
||||
- `signaling-message-handler.ts`, `server-membership-signaling-handler.ts`
|
||||
- `room-signaling-connection.ts`, `rooms.effects.ts`
|
||||
- `server-directory` health / canonical endpoint rules
|
||||
- Server: `server/src/websocket/handler.ts` (out of default scope — ask)
|
||||
|
||||
---
|
||||
|
||||
## Proof of done (signaling)
|
||||
|
||||
1. Two clients join same room: both appear in member lists within seconds of identify.
|
||||
2. Kill signal process; both recover presence without manual leave/rejoin.
|
||||
3. User with rooms on two URLs: leave on URL A does not remove voice peer still shared only via URL B.
|
||||
4. Fresh connect: first outbound after open is identify; join never accepted unauthenticated (server log / test).
|
||||
5. Alias URLs with same `serverInstanceId` collapse before room reconnect.
|
||||
|
||||
---
|
||||
|
||||
## Fix order notes
|
||||
|
||||
Usually fix **auth provision (`04`)** before deep signaling surgery — many “connection” bugs are missing identify. Then harden identify credential resolution and affinity. Instrument `connectionScope`, actor id, and signal URL on every join in debug builds.
|
||||
@@ -1,130 +0,0 @@
|
||||
# 06 — Voice & WebRTC bugs
|
||||
|
||||
> **User theme:** “Voice is broken / one-way / connecting forever / works until reconnect.”
|
||||
> **Severity:** P0.
|
||||
|
||||
---
|
||||
|
||||
## How voice is supposed to work (user view)
|
||||
|
||||
1. Click a voice channel (or answer a call).
|
||||
2. Mic turns on (unless muted); you appear in the channel roster for others.
|
||||
3. You hear everyone in **that** channel; they hear you.
|
||||
4. Mute / deafen / camera / screen share behave Discord-like.
|
||||
5. Navigate away → floating controls; voice continues.
|
||||
6. Joining another voice target auto-leaves the previous one.
|
||||
7. Second device shows you’re in voice elsewhere; Join takes over.
|
||||
8. After a short network blip, voice returns without restarting the app.
|
||||
|
||||
Voice is **not** supposed to depend on the data channel for audio itself — but DC carries control/state; broken DC recovery currently **rebuilds the whole peer**, which drops media too (`07`).
|
||||
|
||||
The users should always be able to hear each other and see each other in joined calls and voice channels. (Not 1 out of 6 cant be heard for 3 users!)
|
||||
|
||||
---
|
||||
|
||||
## How voice is supposed to work (system)
|
||||
|
||||
| Piece | Responsibility |
|
||||
|-------|----------------|
|
||||
| `voice-session` | Session metadata, floating UI, settings, exclusivity, takeover rules |
|
||||
| `voice-connection` | Facade, VAD, per-peer playback gain |
|
||||
| `MediaManager` | getUserMedia, RNNoise, gain, same-channel track routing |
|
||||
| `PeerConnectionManager` | RTCPeerConnection, negotiation, ICE |
|
||||
| Signaling | `offer`/`answer`/`ice_candidate` relay; `voice_state`; `voice_client_takeover` |
|
||||
| ICE | STUN defaults; TURN optional via settings — **no bundled TURN** |
|
||||
|
||||
Initiator election: deterministic compare of local id vs peer `oderId` so only one side offers.
|
||||
|
||||
Audio routing: attach/detach mic based on matching `voiceState.roomId` + `serverId`. Playback similarly scoped.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
### 1 — Cross-signal initiator / politeness uses home id
|
||||
|
||||
**Symptom:** Peers never connect or glare forever when users have different home servers / foreign actor ids in the room.
|
||||
|
||||
**Mechanism**
|
||||
|
||||
- Presence peer ids = **per-server actor** `oderId`.
|
||||
- `getLocalOderId` / polite-peer path often uses `getIdentifyCredentials()` → **home** id (`signaling-transport-handler.ts`, `realtime-session.service.ts`, `negotiation.ts`).
|
||||
- Election `localOderId < peerId` inconsistent across clients → dual offer, dual wait, or stuck non-initiator.
|
||||
|
||||
**Fix direction:** elect and politeness using **per-signal-url** identify credentials (`getIdentifyCredentialsForSignalUrl(peerSignalUrl)` / room source URL), not home-only.
|
||||
|
||||
### 2 — Voice allow-list misses peer map key → one-way / silence
|
||||
|
||||
**Symptom:** Connected peer, speaking UI maybe wrong, no audio out or in.
|
||||
|
||||
**Mechanism:** `syncOutgoingVoiceRouting` / playback only recognizes certain aliases (`id` / `oderId` / `peerId`). If `activePeerConnections` key is another alias → track detached.
|
||||
|
||||
### 3 — Silent give-up after reconnect budget
|
||||
|
||||
**Symptom:** After ~60s of failures, voice never returns; no error toast.
|
||||
|
||||
**Constants:** `PEER_RECONNECT_MAX_ATTEMPTS = 12`, interval 5s. Tracker deleted; no UI.
|
||||
|
||||
### 4 — Transient signaling drops offers during WS reconnect
|
||||
|
||||
Offers/ICE classified transient may be deferred/dropped while socket reconnecting → half-open peers; depends on fallback offer timers (`USER_JOINED_FALLBACK`, non-initiator give-up 5s).
|
||||
|
||||
### 5 — DC recovery tears down media
|
||||
|
||||
Closed control channel → `removePeer` + full reconnect (`peer-recovery.ts`). Docs claim soft `replaceDataChannel` preserving AV — **code does not do that** (`07`, `10`).
|
||||
|
||||
### 6 — Auth / presence missing
|
||||
|
||||
If identify/join failed (`04`/`05`), no RTC relay eligibility / no peer discovery → empty voice.
|
||||
|
||||
### 7 — Same-channel filter false negatives
|
||||
|
||||
Remote `voice_state` missing/stale channel ids → locally mute peer while UI still lists them in channel.
|
||||
|
||||
### 8 — No TURN by default
|
||||
|
||||
Symmetric NAT / strict firewalls fail ICE with STUN-only. User-configurable TURN exists; many installs never set it. Product decision needed: ship defaults vs document limitation.
|
||||
|
||||
### 9 — Multi-device ownership races
|
||||
|
||||
Takeover / heartbeat / `voiceActive` routing wrong → offers hit passive device; active device silent.
|
||||
|
||||
---
|
||||
|
||||
## Code vs docs
|
||||
|
||||
| Doc claim | Code |
|
||||
|-----------|------|
|
||||
| Soft DC renegotiation preserves media | Always full peer recreate on closed DC |
|
||||
| Deterministic initiator from logical peer ids | Uses home identify credentials in several paths |
|
||||
| TURN supported | Configurable only; not default |
|
||||
|
||||
---
|
||||
|
||||
## Key files
|
||||
|
||||
- `domains/voice-session/`, `domains/voice-connection/`
|
||||
- `infrastructure/realtime/media/media.manager.ts`
|
||||
- `peer-connection-manager/**`, `negotiation.ts`, `peer-recovery.ts`
|
||||
- `signaling-message-handler.ts` (server_users / user_joined offers)
|
||||
- `ice-server-settings.service.ts`, `realtime.constants.ts`
|
||||
- Feature: `agents-docs/features/voice-webrtc.md`
|
||||
|
||||
---
|
||||
|
||||
## Proof of done
|
||||
|
||||
1. Two users same home signal, same voice channel: bidirectional audio < 5s after both join.
|
||||
2. Two users **different home signals**, same foreign-hosted room (both provisioned): bidirectional audio.
|
||||
3. Toggle mute/deafen; camera; screen share request path.
|
||||
4. Kill Wi‑Fi 15s: recovers or shows actionable error (not silent forever).
|
||||
5. Second client takeover: first stops transmitting; second owns mic.
|
||||
6. Regression test for initiator election with mismatched home vs actor ids.
|
||||
|
||||
---
|
||||
|
||||
## Interview prompts (when implementing)
|
||||
|
||||
- Soft DC replace vs keep full rebuild but fix media reattach + UX error?
|
||||
- Ship public TURN defaults or document “requires open NAT / user TURN”?
|
||||
- Instrument-only first week vs behavior fix first?
|
||||
@@ -1,99 +0,0 @@
|
||||
# 07 — Data channel drops & recovery
|
||||
|
||||
> **User theme:** “Connection drops / chat and files die / voice dies after a blip.”
|
||||
> **Severity:** P0/P1 — shared control plane for chat sync, attachments, emoji, screen control, and currently media (because recovery rebuilds the whole PC).
|
||||
|
||||
---
|
||||
|
||||
## How the data channel is supposed to work (user view)
|
||||
|
||||
- Once you’re in a community with other people, messages, files, emoji, and many live updates “just sync.”
|
||||
- Brief network glitches should self-heal.
|
||||
- You should not need to restart the app to get chat syncing again.
|
||||
- Voice should ideally survive control-plane blips (product docs claim this; code currently does not).
|
||||
- Users should sync all but only load a portion into ram for viewing so the app doesn't crash of high ram usage.
|
||||
---
|
||||
|
||||
## How the data channel is supposed to work (system)
|
||||
|
||||
- Single **ordered** RTCDataChannel per peer pair (label typically chat/control).
|
||||
- Carries: chat events, inventory sync, attachments, avatar/emoji chunks, voice/screen control, pings, plugin bus, game activity, etc.
|
||||
- Back-pressure: high 4MB / low 1MB watermarks.
|
||||
- Ping every 5s for RTT.
|
||||
- On failure, recovery should restore control **and** ensure inventory/resync runs when channel reopens.
|
||||
|
||||
### Documented recovery (README / voice-webrtc.md)
|
||||
|
||||
1. Non-fatal error on **open** channel → request voice-state snapshot on same channel.
|
||||
2. **Closed** channel → initiator renegotiates **new DC on existing PC** (preserve AV); non-initiator waits then full rebuild if missing.
|
||||
3. Closing-but-not-closed → short grace (2.5s).
|
||||
4. `replaceDataChannel` adopts the new channel.
|
||||
|
||||
### Actual recovery (`peer-recovery.ts`)
|
||||
|
||||
1. Closed → `repairUnavailableDataChannel` → `removePeer` + `attemptPeerReconnect` / `schedulePeerReconnect` (**full PC teardown**).
|
||||
2. Closing → wait `DATA_CHANNEL_RECOVERY_GRACE_MS` (2.5s) → same full recreate.
|
||||
3. `replaceDataChannel` exists on the manager and is wired into handlers, but **recovery path never calls it**; specs assert it is **not** called in several cases.
|
||||
4. After 12 reconnect attempts (~60s): abandon **silently**.
|
||||
|
||||
**This is a documented lie** — treat README paragraph as aspirational until code matches or docs are corrected in the same PR as a deliberate decision.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
### 1 — Full rebuild drops audio/video on every DC close
|
||||
|
||||
User hears a “drop” even when ICE media might have survived. Cascades into voice bug reports.
|
||||
|
||||
### 2 — Silent abandon
|
||||
|
||||
No toast, no “Reconnect” CTA, no automatic retry on later `user_joined`. Mesh looks permanently broken until navigation/restart.
|
||||
|
||||
### 3 — Live chat limp vs history dead
|
||||
|
||||
While DC down, `chat_message` WS fallback may still deliver **live** text. Inventory sync is **DC-only** → late joiners / catch-up fail until P2P returns (`08`).
|
||||
|
||||
### 4 — Large payloads kill shared channel
|
||||
|
||||
Custom emoji / attachment floods can stress or close the shared ordered channel (lessons). One feature outage becomes total control-plane outage.
|
||||
|
||||
### 5 — Attachment announce vs message ordering
|
||||
|
||||
`file-announce` on DC can beat `chat-message` on WS → auto-download gives up unless re-queued on message bind (lesson; verify still present when touching attachments).
|
||||
|
||||
### 6 — Replacement channel race
|
||||
|
||||
If soft-replace is reintroduced, must close old channel to release SCTP (voice-webrtc changelog). Current full rebuild avoids that class but at higher cost.
|
||||
|
||||
---
|
||||
|
||||
## Key files
|
||||
|
||||
- `peer-connection-manager/messaging/data-channel.ts`
|
||||
- `peer-connection-manager/recovery/peer-recovery.ts` (+ specs)
|
||||
- `peer-connection.manager.ts` (`replaceDataChannel`)
|
||||
- `realtime.constants.ts`
|
||||
- Consumers: chat sync effects, attachment transfer, custom emoji chunking, screen-share request
|
||||
|
||||
---
|
||||
|
||||
## Fix directions (interview)
|
||||
|
||||
| Option | Idea | Tradeoff |
|
||||
|--------|------|----------|
|
||||
| **A (align code to docs)** | Implement true soft DC replace on connected PC; full rebuild only if PC not connected | Harder; matches user expectation for voice survival |
|
||||
| **B (align docs to code)** | Keep full rebuild; fix reattach + force inventory on reopen; **surface give-up UX** | Faster; still interrupts voice |
|
||||
| **C** | Separate unreliable channel for bulk (files/emoji) vs reliable small control | Larger design |
|
||||
|
||||
Recommend starting with **instrumentation + B’s UX/resync**, then **A** if voice drop rate stays high.
|
||||
|
||||
---
|
||||
|
||||
## Proof of done
|
||||
|
||||
1. Force-close DC in debug: control messages resume; inventory runs; user sees progress or success.
|
||||
2. If soft-replace chosen: audio continues through DC replace (automated or manual with metrics).
|
||||
3. After max attempts: visible error + manual retry works.
|
||||
4. Attachment + emoji transfer during recovery does not deadlock the mesh.
|
||||
5. Update `realtime/README.md` + `voice-webrtc.md` in the same change set so they match behavior.
|
||||
@@ -1,110 +0,0 @@
|
||||
# 08 — Messaging visibility (“messages not seen”)
|
||||
|
||||
> **User theme:** “I sent a message but they don’t see it / history missing / only some devices have chat.”
|
||||
> **Severity:** P0.
|
||||
|
||||
---
|
||||
|
||||
## How messaging is supposed to work (user view)
|
||||
|
||||
### Server text channels
|
||||
|
||||
- Send in a text channel → everyone currently in that community sees it quickly.
|
||||
- If someone was offline or just joined, they still get recent history after connecting to peers (or from their other logged-in device).
|
||||
- Edits, deletes, reactions converge.
|
||||
- Typing indicators are ephemeral.
|
||||
- The **signal server does not keep a chat log** — history lives on clients.
|
||||
|
||||
### Direct messages
|
||||
|
||||
- 1:1 and group DMs deliver even without a shared community when signaling can reach the peer.
|
||||
- Delivery ticks: queued → sent → delivered → acknowledged (monotonic).
|
||||
- Offline: queue until peer/network returns.
|
||||
|
||||
### Multi-device
|
||||
|
||||
- Second device receives live + catch-up via `account_sync` when siblings are online.
|
||||
|
||||
---
|
||||
|
||||
## How messaging is supposed to work (system)
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| P2P `chat-message` / revisions | Primary live + sync plane |
|
||||
| WS `chat_message` | Narrow **live** fallback to room members |
|
||||
| Inventory / sync-batch | Catch-up on DC (limit 20_000, chunk 200) |
|
||||
| `account_sync` chat batches | Sibling devices |
|
||||
| DM PeerDelivery | DC → signaling → offline queue |
|
||||
|
||||
Feature contract: `agents-docs/features/messaging.md`.
|
||||
Domain: `domains/chat/`, `domains/direct-message/`, `store/messages/`.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
### 1 — Invisible membership → no live fallback
|
||||
|
||||
If identify/join failed (`04`/`05`), user is not in server membership → server never broadcasts `chat_message` to them; peers may not offer DC. **Classic “chats don’t sync for multi-client users”** root cause (serialized identify).
|
||||
|
||||
### 2 — Live works, history doesn’t
|
||||
|
||||
DC down: live WS fallback OK; inventory never runs → “they only see new messages after refresh if a peer happens to sync later” / empty history for late joiners.
|
||||
|
||||
### 3 — Sync poll too slow after “clean” cycle
|
||||
|
||||
Fast poll 10s while catching up; **15 min** when clean. A false “clean” leaves long windows without repair.
|
||||
|
||||
### 4 — Cross-signal identity forks DMs
|
||||
|
||||
Incoming DM `conversationId` carries foreign actor id → second empty thread; replies invisible on the thread the user is watching (`09`). Lessons claim canonicalize/merge — **symbols not in tree**.
|
||||
|
||||
### 5 — Recipient alias miss
|
||||
|
||||
DM/call ignored if local admission only checks home id. Inbound DM aliases largely fixed; verify call + any new surfaces.
|
||||
|
||||
### 6 — Attachment-only emptiness
|
||||
|
||||
Message text arrives; image stuck “Waiting…” — announce/bind race (lesson). Looks like “message incomplete / not really received.”
|
||||
|
||||
### 7 — NgRx prune confusion
|
||||
|
||||
Inactive rooms pruned to 100 messages in memory; DB still has more. User switching rooms may think history vanished until reload/sync — document vs bug.
|
||||
|
||||
### 8 — Doc lie on inventory cap
|
||||
|
||||
Chat domain README still says **1000**; code `INVENTORY_LIMIT = 20_000`.
|
||||
|
||||
---
|
||||
|
||||
## Investigation checklist (agents)
|
||||
|
||||
1. Did both users `identify` + `join_server` on the **same** signal URL as the room?
|
||||
2. Is there an open DC between them? (`connectedPeers`, debug metrics)
|
||||
3. Does live send emit both DC and `chat_message`?
|
||||
4. On receive, is `roomId` in saved/current rooms?
|
||||
5. For DM: conversation id aliases; delivery state machine stuck at QUEUED?
|
||||
6. Multi-device: `account_sync_peer_online` fired; batches received?
|
||||
|
||||
---
|
||||
|
||||
## Proof of done
|
||||
|
||||
1. Two clients: send N messages with DC disabled (force) → live still appears via WS.
|
||||
2. Re-enable DC → inventory brings missing history.
|
||||
3. Third client late join → receives recent history from a peer with DB.
|
||||
4. Cross-home users in foreign room: messages visible both ways without authorize prompt.
|
||||
5. Cross-signal DM: single conversation thread; replies visible to both.
|
||||
6. Multi-device: second device gets `chat-sync-batch` after identify.
|
||||
7. Regression covering identify-before-join (presence + chat broadcast).
|
||||
|
||||
---
|
||||
|
||||
## Fix order
|
||||
|
||||
1. Auth + identify (`04`/`05`) — without presence, messaging “fixes” are theater.
|
||||
2. Ensure fallback + DC resync on repair (`07`).
|
||||
3. Identity canonicalize for DMs (`09`).
|
||||
4. Attachment re-queue invariants.
|
||||
5. Correct stale chat README inventory number.
|
||||
@@ -1,113 +0,0 @@
|
||||
# 09 — Cross-signal identity (calls, DMs, voice routing)
|
||||
|
||||
> **User theme:** “Calls don’t ring / I’m In Voice alone / DMs fork / voice fails only with people on other homes.”
|
||||
> **Severity:** P0.
|
||||
> **Coupling:** Depends on silent provision (`04`) creating foreign actor ids at all.
|
||||
|
||||
---
|
||||
|
||||
## How identity is supposed to work (user view)
|
||||
|
||||
- You have one profile on this device.
|
||||
- People may see slightly different usernames on other signal hosts (suffix / `#prefix` tag) — still you.
|
||||
- Calling or DMing someone you met in any community should reach **them**, not a ghost.
|
||||
- You should never end up with two chat threads that are secretly the same person.
|
||||
- Voice in a shared room should work even if you registered on different home signal servers.
|
||||
|
||||
---
|
||||
|
||||
## How identity is supposed to work (system)
|
||||
|
||||
| Id | Meaning |
|
||||
|----|---------|
|
||||
| Home user id | Local profile / NgRx `User.id` |
|
||||
| Foreign actor id | Credential `userId` for that signal URL |
|
||||
| Peer map key | Usually the `oderId` seen on that signal’s presence |
|
||||
| `targetUserId` on WS relay | Must equal callee’s **connected** `oderId` on that server |
|
||||
| DM `conversationId` | Should canonicalize to one thread per human pair on this device |
|
||||
|
||||
Self-admission for inbound events must accept **all** aliases: home id, entity id, peer id, every valid provisioned credential user id.
|
||||
|
||||
Outbound delivery must pick a **routable** id (one the signaling server can map to an open connection), not merely the home id stored on a people card.
|
||||
|
||||
---
|
||||
|
||||
## What is implemented vs claimed
|
||||
|
||||
| Capability | Status (2026-08-12 tree) |
|
||||
|------------|--------------------------|
|
||||
| Inbound direct-call alias admission + normalize | **Present** — `direct-call-participant-identity.rules.ts` |
|
||||
| DM self-id alias sets | **Present** — `direct-message-identity.rules.ts` (narrower than lesson’s conversation merge) |
|
||||
| Outbound `collectRecipientDeliveryCandidateIds` / `pickRoutableRecipientId` | **Absent** — lesson describes as if shipped |
|
||||
| `DirectCallService.resolveRoutableRecipientId` / `recipientUnreachable` UX | **Absent** |
|
||||
| `resolveDirectConversationId` / `mergeAliasDirectConversations` | **Absent** |
|
||||
| Room join uses `resolveActorUserIdForServer` | **Present** — `room-signaling-connection.ts` |
|
||||
| Initiator election uses per-signal actor id | **Weak / home-biased** — see `06` |
|
||||
|
||||
**Treat LESSONS entries for outbound call routing and DM canonicalize as specifications of unfinished work**, not as completed history.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
### 1 — Outbound call: silent “In Voice”
|
||||
|
||||
Caller joins call session; `PeerDeliveryService` cannot resolve signaling peer id (home id ≠ connected actor id); ring never sent or wrong `targetUserId`; delivery result ignored.
|
||||
|
||||
### 2 — Inbound call: dropped ring (historical)
|
||||
|
||||
Fixed for admission aliases; still verify e2e `dm-header-call-ring` across secondary signal registration. Do not assume outbound is fixed because inbound is.
|
||||
|
||||
### 3 — DM thread fork
|
||||
|
||||
Incoming messages keyed by foreign actor conversation id → UI shows empty home-id thread; replies land elsewhere.
|
||||
|
||||
### 4 — Voice peer election / routing alias miss
|
||||
|
||||
Home vs actor mismatch → no PC or one-way audio (`06`).
|
||||
|
||||
### 5 — People search / friends store home ids only
|
||||
|
||||
Cards display home identity; without alias expansion at send time, every cross-signal action is fragile.
|
||||
|
||||
---
|
||||
|
||||
## Key files
|
||||
|
||||
- `domains/direct-call/` (+ `direct-call-participant-identity.rules.ts`)
|
||||
- `domains/direct-message/` (+ `direct-message-identity.rules.ts`, `PeerDeliveryService`)
|
||||
- `domains/authentication/` credential store + `resolveActorUserIdForServer`
|
||||
- `infrastructure/realtime/signaling-transport-handler.ts` (home vs per-URL credentials)
|
||||
- Lessons (aspirational): outbound routing + DM canonicalize sections in `LESSONS.md`
|
||||
- E2E intent: `e2e/tests/voice/dm-header-call-ring.spec.ts` (scope expand if needed)
|
||||
|
||||
---
|
||||
|
||||
## Recommended implementation sequence
|
||||
|
||||
1. **Instrument** one cross-home repro: log home id, actor ids, peer map keys, `targetUserId`, conversation ids.
|
||||
2. **Outbound delivery** — implement lesson APIs for real: collect aliases, pick routable, always attempt send, surface unreachable error (stop fake In Voice).
|
||||
3. **DM canonicalize + merge** — single thread; remap inbound; merge duplicates on load.
|
||||
4. **Voice initiator / routing** — per-signal local actor id (`06`).
|
||||
5. Rewrite lesson examples if names differ so future agents don’t hunt phantom files.
|
||||
|
||||
---
|
||||
|
||||
## Proof of done
|
||||
|
||||
1. User1 home A, User2 home B, meet in room on A: DM-header call rings B’s modal; B answers; both hear audio.
|
||||
2. Same pair: DM replies appear in **one** thread on both clients.
|
||||
3. People-card call when only shared presence is under actor id: still rings or shows **unreachable** (never silent In Voice).
|
||||
4. Unit tests for alias collect/pick and conversation merge.
|
||||
5. E2E cross-signal call + DM (expand scope with user approval for `e2e/`).
|
||||
|
||||
---
|
||||
|
||||
## Interview choices
|
||||
|
||||
| Option | Focus first |
|
||||
|--------|-------------|
|
||||
| **A (recommended)** | Outbound routable id + unreachable UX |
|
||||
| **B** | DM canonicalize/merge first |
|
||||
| **C** | Voice initiator per-signal id first |
|
||||
| **D** | Full identity service refactor (large — avoid in emergency) |
|
||||
@@ -1,101 +0,0 @@
|
||||
# 10 — Code lies & documentation debt
|
||||
|
||||
> Agents must **verify symbols in the tree** before trusting lessons or READMEs. This file lists known mismatches found 2026-08-12.
|
||||
|
||||
---
|
||||
|
||||
## Critical lies (behavior-affecting)
|
||||
|
||||
### L1 — Soft data-channel replace (docs yes, code no)
|
||||
|
||||
**Claims:** `toju-app/.../realtime/README.md` (Data channel section), `agents-docs/features/voice-webrtc.md`.
|
||||
|
||||
**Reality:** `repairUnavailableDataChannel` always `removePeer` + reconnect. `replaceDataChannel` not used by recovery; specs expect it not called.
|
||||
|
||||
**Action:** Either implement soft replace or rewrite docs in the same PR as the recovery decision (`07`).
|
||||
|
||||
### L2 — LESSONS outbound call routing “fixed”
|
||||
|
||||
**Claims:** `LESSONS.md` — `peer-delivery-identity.rules.ts`, `collectRecipientDeliveryCandidateIds`, `pickRoutableRecipientId`, `resolveRoutableRecipientId`, `call.errors.recipientUnreachable`.
|
||||
|
||||
**Reality:** **No matches** in `toju-app/`. Phantom APIs.
|
||||
|
||||
**Action:** Implement (`09`) or rewrite lesson as “desired / unfinished” with Status.
|
||||
|
||||
### L3 — LESSONS DM conversation canonicalize “fixed”
|
||||
|
||||
**Claims:** `resolveDirectConversationId`, `mergeAliasDirectConversations`, `direct-message-conversation-identity.rules.ts`, etc.
|
||||
|
||||
**Reality:** **Absent.** Only narrower `direct-message-identity.rules.ts`.
|
||||
|
||||
**Action:** Same as L2.
|
||||
|
||||
### L4 — Home identify credentials used as “local peer id” for negotiation
|
||||
|
||||
**Claims:** Deterministic initiator from logical peer ids (implies per-room identity).
|
||||
|
||||
**Reality:** `getIdentifyCredentials()` returns **home** credential; used for polite peer / localOderId paths while presence uses actor ids.
|
||||
|
||||
**Action:** Fix in voice/identity packets (`06`/`09`); update README after.
|
||||
|
||||
---
|
||||
|
||||
## Medium lies (wrong paths / stale numbers)
|
||||
|
||||
### L5 — Authentication domain README API paths
|
||||
|
||||
**Claims:** `POST /api/auth/login`, `/api/auth/register`.
|
||||
|
||||
**Reality:** `/api/users/login`, `/api/users/register`.
|
||||
|
||||
### L6 — Chat domain README inventory cap
|
||||
|
||||
**Claims:** capped at **1 000** messages.
|
||||
|
||||
**Reality:** `INVENTORY_LIMIT = 20_000` (messaging feature doc correct).
|
||||
|
||||
### L7 — `shared-kernel/signaling-contracts.ts` as wire authority
|
||||
|
||||
**Claims (implicit):** types like `join` / `leave` / `chat` / `ice-candidate`.
|
||||
|
||||
**Reality:** Feature `signaling.md` correctly says **do not treat as authoritative**. Live types use `join_server`, `chat_message`, `ice_candidate`, etc.
|
||||
|
||||
**Action:** Mark file deprecated or align types; never generate client sends from it blindly.
|
||||
|
||||
---
|
||||
|
||||
## Soft / incomplete docs (not lies, but traps)
|
||||
|
||||
| Topic | Note |
|
||||
|-------|------|
|
||||
| Auth feature doc vs missing secret | Doc describes intended offline/authorize rules; primary bug is still open (user story). |
|
||||
| TURN | Documented STUN-only defaults — accurate; users may think voice “should always work on restrictive NAT”. |
|
||||
| Domain auth README | Oversimplified sequence diagram (no provision secret / multi-credential). |
|
||||
| Handoff silent auth | Accurate research; not a code lie — unfinished work. |
|
||||
|
||||
---
|
||||
|
||||
## Verification commands for agents
|
||||
|
||||
```bash
|
||||
# Phantom lesson APIs (should be empty until implemented)
|
||||
rg -n 'pickRoutableRecipientId|resolveDirectConversationId|mergeAliasDirectConversations|peer-delivery-identity' toju-app
|
||||
|
||||
# DC recovery behavior
|
||||
rg -n 'repairUnavailableDataChannel|replaceDataChannel' toju-app/src/app/infrastructure/realtime
|
||||
|
||||
# Inventory limit
|
||||
rg -n 'INVENTORY_LIMIT' toju-app/src/app/domains/chat
|
||||
|
||||
# Auth paths
|
||||
rg -n 'users/login|auth/login' toju-app/src/app/domains/authentication
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy for this emergency
|
||||
|
||||
1. Prefer **feature docs + code** over domain README diagrams when they conflict.
|
||||
2. Prefer **code + specs** over LESSONS examples when symbols missing.
|
||||
3. When fixing behavior, **fix the lying doc in the same PR**.
|
||||
4. When a lesson was aspirational, relabel it explicitly so the next agent does not skip the work.
|
||||
@@ -1,153 +0,0 @@
|
||||
# 11 — Fix priority plan (systematic waves)
|
||||
|
||||
> Execute in order unless the user explicitly reprioritizes.
|
||||
> Each wave lists **user outcome**, **work**, **depends on**, **proof**.
|
||||
> Checkboxes are for agents to update when a wave is done.
|
||||
|
||||
---
|
||||
|
||||
## Guiding principles
|
||||
|
||||
1. **User-visible done** > unit-green alone.
|
||||
2. Fix **identity + auth** before chasing random WebRTC knobs — many voice/chat bugs are “never identified / wrong id.”
|
||||
3. **Interview before implement** on each wave (repo rule) unless opted out / handoff already approved.
|
||||
4. Stay in `toju-app` + targeted `electron`; ask once before `server/` or full `e2e/`.
|
||||
5. Correct lying docs in the same change (`10`).
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 — Shared baseline (half day)
|
||||
|
||||
- [ ] **0.1** Read `00-README.md`, `01`, `03`, `10`.
|
||||
- [ ] **0.2** Build a **cross-home repro matrix** (manual or scripted):
|
||||
- Same home, same room
|
||||
- Different homes, room on home A
|
||||
- Different homes, room on foreign B (provision required)
|
||||
- DM + call between different homes
|
||||
- [ ] **0.3** Enable WebRTC/signaling debug logs; capture home id, actor ids, peer keys, signal URLs on failure.
|
||||
- [ ] **0.4** Confirm whether provision secret exists after fresh login vs after restart (Electron + browser).
|
||||
|
||||
**Proof:** Written repro notes attached to the next handoff (short).
|
||||
|
||||
---
|
||||
|
||||
## Wave 1 — Silent multi-signal auth (P0)
|
||||
|
||||
**User outcome:** Never bounced to authorize login just for touching another signal server.
|
||||
|
||||
- [ ] **1.1** Interview + implement provision-secret ensure on restore (see `04`, user story options A–D; recommend A).
|
||||
- [ ] **1.2** Harden `auth_required` / `auth_error` home vs foreign handling (no false `SESSION_EXPIRED`).
|
||||
- [ ] **1.3** Offline endpoints never navigate authorize (regression test).
|
||||
- [ ] **1.4** Prove J4 + restart path (`02`).
|
||||
|
||||
**Depends on:** Wave 0.
|
||||
**Pack:** `04`, story `agents-docs/user-stories/silent-cross-signal-server-auth.md`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 2 — Presence identify/join integrity (P0)
|
||||
|
||||
**User outcome:** When you open a room, others see you; you receive live chat/voice roster.
|
||||
|
||||
- [ ] **2.1** Verify `getIdentifyCredentialsForSignalUrl` store fallback on every reconnect path.
|
||||
- [ ] **2.2** Ensure room connect always identify → join with **actor** id for `sourceUrl`.
|
||||
- [ ] **2.3** Alias canonicalization / cold-start health wait still correct.
|
||||
- [ ] **2.4** Prove two-client presence + live `chat_message` after signal restart.
|
||||
|
||||
**Depends on:** Wave 1 (foreign rooms).
|
||||
**Pack:** `05`, `08`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 3 — Cross-signal outbound identity (P0)
|
||||
|
||||
**User outcome:** Calls ring the real person or show a clear error; DMs stay one thread.
|
||||
|
||||
- [ ] **3.1** Implement outbound routable recipient selection + unreachable UX (`09`, lesson L2).
|
||||
- [ ] **3.2** Implement DM conversation canonicalize + merge (`09`, lesson L3).
|
||||
- [ ] **3.3** Relabel/fix LESSONS so examples match shipped symbols.
|
||||
- [ ] **3.4** Prove cross-home call + DM reply visibility.
|
||||
|
||||
**Depends on:** Wave 1. Can parallelize lightly with Wave 2 if staffing allows.
|
||||
**Pack:** `09`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 4 — Voice negotiation & routing (P0)
|
||||
|
||||
**User outcome:** Bidirectional voice in shared channels, including cross-home users; reconnect either works or errors visibly.
|
||||
|
||||
- [ ] **4.1** Per-signal-url local actor id for initiator election + polite peer (`06` failure 1).
|
||||
- [ ] **4.2** Voice allow-list includes all peer-map aliases.
|
||||
- [ ] **4.3** After reconnect budget: user-visible failure + retry action.
|
||||
- [ ] **4.4** Decide TURN product stance (document vs defaults).
|
||||
- [ ] **4.5** Prove same-home + cross-home voice; takeover; blip recovery.
|
||||
|
||||
**Depends on:** Waves 1–2; benefits from 3.
|
||||
**Pack:** `06`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 5 — Data channel recovery honesty (P0/P1)
|
||||
|
||||
**User outcome:** Control plane self-heals; chat history catch-up resumes; voice doesn’t mysteriously die without explanation.
|
||||
|
||||
- [ ] **5.1** Interview: soft replace (A) vs full rebuild + UX/resync (B) (`07`).
|
||||
- [ ] **5.2** Implement choice; force inventory/resync on DC reopen.
|
||||
- [ ] **5.3** Align `realtime/README.md` + `voice-webrtc.md` with code (`10` L1).
|
||||
- [ ] **5.4** Prove DC force-close recovery + no silent abandon.
|
||||
|
||||
**Depends on:** Wave 4 preferred (so voice metrics make sense).
|
||||
**Pack:** `07`, `08`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 6 — Messaging catch-up & attachments (P1)
|
||||
|
||||
**User outcome:** Late joiners get history; images/files don’t stick on “Waiting…”.
|
||||
|
||||
- [ ] **6.1** Verify/fix inventory trigger on peer + DC repair.
|
||||
- [ ] **6.2** Re-verify attachment announce/bind re-queue invariants.
|
||||
- [ ] **6.3** Fix chat README inventory cap (`10` L6).
|
||||
- [ ] **6.4** Prove late-join history + image sync under reorder stress.
|
||||
|
||||
**Depends on:** Waves 2 + 5.
|
||||
**Pack:** `08`.
|
||||
|
||||
---
|
||||
|
||||
## Wave 7 — Doc & contract cleanup (P2, continuous)
|
||||
|
||||
- [ ] **7.1** Fix auth domain README paths (`10` L5).
|
||||
- [ ] **7.2** Deprecate or fix `signaling-contracts.ts` warning banner.
|
||||
- [ ] **7.3** Sync domain READMEs that oversimplify multi-credential auth.
|
||||
- [ ] **7.4** Clear or refresh `agents-docs/HANDOFF.md` after each finished wave.
|
||||
|
||||
---
|
||||
|
||||
## Suggested staffing (multiple agents)
|
||||
|
||||
| Agent | Wave |
|
||||
|-------|------|
|
||||
| Auth agent | 1 |
|
||||
| Realtime/signaling agent | 2, 5 |
|
||||
| Identity/DM/call agent | 3 |
|
||||
| Voice agent | 4 |
|
||||
| Chat/attachments agent | 6 |
|
||||
| Docs agent | 7 (or same PR as each fix) |
|
||||
|
||||
Do **not** start Wave 4 “TURN tuning” before Waves 1–3 — it wastes time on NAT while identity is wrong.
|
||||
|
||||
---
|
||||
|
||||
## Definition of “emergency over”
|
||||
|
||||
All of the following true on a two-signal-server manual matrix:
|
||||
|
||||
1. No spurious authorize/login while home session valid.
|
||||
2. Presence mutual in shared rooms.
|
||||
3. Bidirectional voice same-home and cross-home.
|
||||
4. Live text + catch-up history.
|
||||
5. Cross-home DM + call succeed or fail loudly.
|
||||
6. DC/WS blip recovers or shows retry UI within ~1 minute.
|
||||
7. Docs for recovery/identity match code (`10` critical lies cleared).
|
||||
@@ -1,129 +0,0 @@
|
||||
# 12 — Agent work packets (copy into new chats)
|
||||
|
||||
> Each packet is one new chat. Attach `@emergency-fix/00-README.md` plus the listed files.
|
||||
> Say: **continue this packet; interview before implement; do not expand scope.**
|
||||
|
||||
---
|
||||
|
||||
## Packet A — Silent foreign auth
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/04-auth-login-bugs.md`
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 1)
|
||||
- `@agents-docs/user-stories/silent-cross-signal-server-auth.md`
|
||||
|
||||
**Goal:** Wave 1 — never show authorize login for normal foreign joins when home session is valid.
|
||||
|
||||
**Out of scope:** Voice negotiation, DM canonicalize, server password hashing changes.
|
||||
|
||||
**First message expectation:** Short interview (options A–D from user story); wait.
|
||||
|
||||
---
|
||||
|
||||
## Packet B — Presence / identify-join
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/05-signaling-multi-server.md`
|
||||
- `@emergency-fix/08-messaging-visibility.md` (membership section)
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 2)
|
||||
|
||||
**Goal:** Mutual presence + live chat broadcast after connect/reconnect; foreign actor id on join.
|
||||
|
||||
**Depends:** Packet A ideally done (or mock credentials present).
|
||||
|
||||
**Out of scope:** Soft DC replace, TURN.
|
||||
|
||||
---
|
||||
|
||||
## Packet C — Outbound call + DM identity
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/09-identity-cross-signal.md`
|
||||
- `@emergency-fix/10-code-lies-doc-debt.md` (L2, L3)
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 3)
|
||||
|
||||
**Goal:** Routable outbound `targetUserId`; unreachable UX; single DM thread across aliases; fix phantom LESSON references when shipping.
|
||||
|
||||
**Out of scope:** Auth provision secret (Packet A); full voice ICE redesign.
|
||||
|
||||
**Note:** LESSONS claim these fixes already exist — **they do not**. Implement, don’t search forever.
|
||||
|
||||
---
|
||||
|
||||
## Packet D — Voice initiator & routing
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/06-voice-webrtc-bugs.md`
|
||||
- `@emergency-fix/03-architecture-map.md`
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 4)
|
||||
|
||||
**Goal:** Cross-home bidirectional voice; per-signal actor initiator election; reconnect UX; TURN product decision.
|
||||
|
||||
**Depends:** A + B strongly; C helpful.
|
||||
|
||||
**Out of scope:** Rewriting entire PeerConnectionManager; server relay policy changes unless proven necessary (ask).
|
||||
|
||||
---
|
||||
|
||||
## Packet E — Data channel recovery
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/07-data-channel-drops.md`
|
||||
- `@emergency-fix/10-code-lies-doc-debt.md` (L1)
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 5)
|
||||
|
||||
**Goal:** Honest recovery (soft replace **or** documented full rebuild + UX + forced resync); docs match code.
|
||||
|
||||
**Interview required:** Option A vs B in `07`.
|
||||
|
||||
---
|
||||
|
||||
## Packet F — Messaging catch-up & attachments
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/00-README.md`
|
||||
- `@emergency-fix/08-messaging-visibility.md`
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 6)
|
||||
- Optional: `agents-docs/features/messaging.md`, `agents-docs/features/attachments.md`
|
||||
|
||||
**Goal:** Late-join history; attachment waiting stuck fixed under reorder; README inventory cap corrected.
|
||||
|
||||
**Depends:** B + E.
|
||||
|
||||
---
|
||||
|
||||
## Packet G — Doc debt sweep
|
||||
|
||||
**Attach**
|
||||
|
||||
- `@emergency-fix/10-code-lies-doc-debt.md`
|
||||
- `@emergency-fix/11-fix-priority-plan.md` (Wave 7)
|
||||
|
||||
**Goal:** Clear remaining L5–L7 and any lies left after A–F; no behavior changes unless a one-line comment/doc only.
|
||||
|
||||
---
|
||||
|
||||
## Handoff one-liner templates
|
||||
|
||||
After finishing a packet, overwrite `agents-docs/HANDOFF.md` and ask the user to start a new chat with:
|
||||
|
||||
```text
|
||||
Continue from @agents-docs/HANDOFF.md and @emergency-fix/11-fix-priority-plan.md — next unchecked wave only.
|
||||
```
|
||||
|
||||
Or jump packets:
|
||||
|
||||
```text
|
||||
Start @emergency-fix/12-agent-work-packets.md Packet C — interview before implement.
|
||||
```
|
||||
@@ -1,485 +0,0 @@
|
||||
# Validated emergency findings — Fable 5 implementation handoff
|
||||
|
||||
**Date:** 2026-08-12
|
||||
**Scope:** `emergency-fix/`, `emergency-fix/e2e-failures/`, Angular client, targeted Electron/auth bridge, signaling server, and relevant Playwright tests.
|
||||
**Method:** Read-only code and test audit. No product code was changed and the E2E suite was not rerun for this report.
|
||||
|
||||
## Executive verdict
|
||||
|
||||
The emergency pack is directionally correct, but it mixes active defects, already-shipped mitigations, hypotheses, and documentation debt. The current app is **not yet demonstrated to be Discord-reliable** despite a green `65/65` Playwright run.
|
||||
|
||||
The highest-confidence failure chain is:
|
||||
|
||||
1. Restored sessions do not ensure a provision secret.
|
||||
2. A foreign signal credential cannot be created.
|
||||
3. The user is sent to authorize login or never identifies/joins.
|
||||
4. Presence is missing, so WebRTC discovery and WebSocket chat fallback fail.
|
||||
5. When peers do connect, negotiation still compares home ids against foreign actor ids.
|
||||
6. A data-channel failure tears down the entire peer connection, including media.
|
||||
7. Reconnect attempts can be exhausted while signaling is offline, then stop silently.
|
||||
|
||||
Independent P0/P1 defects also exist in direct-message identity, call delivery, and message synchronization:
|
||||
|
||||
- inbound DMs trust an actor-id conversation id and can fork a second local thread;
|
||||
- an outbound call enters local voice state before delivery is known, and delivery failure is ignored;
|
||||
- a timed-out inventory cycle is marked “clean” without proving convergence, delaying the next poll for 15 minutes.
|
||||
|
||||
## Acceptance target
|
||||
|
||||
This emergency is not complete until two users with different home signal servers can:
|
||||
|
||||
- join the same foreign-hosted community without another login prompt;
|
||||
- see each other online and in the correct voice channel;
|
||||
- exchange live and catch-up text messages in both directions;
|
||||
- place and answer a private call with bidirectional audio;
|
||||
- keep one DM thread for the same human across actor aliases;
|
||||
- transfer a file and verify received bytes;
|
||||
- use camera and screen share while voice remains healthy;
|
||||
- recover from a signal-server restart and a data-channel failure, or receive a visible retry action within one minute.
|
||||
|
||||
## Finding classification
|
||||
|
||||
- **Confirmed:** direct current-code mechanism.
|
||||
- **Partial:** some mechanism exists, but the report overstates or misattributes it.
|
||||
- **Mitigated:** code already contains the reported fix; retain as a regression test.
|
||||
- **Unproven:** plausible but needs reproduction evidence.
|
||||
|
||||
## Confirmed findings
|
||||
|
||||
### F1 — Restored sessions can lack a provision secret
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** A logged-in user opens a foreign room or invite and is shown `/login?mode=authorize`, or remains invisible on that signal server.
|
||||
|
||||
`loadCurrentUserSuccess` migrates the home credential and immediately attempts foreign provisioning:
|
||||
|
||||
- [`users.effects.ts`](../toju-app/src/app/store/users/users.effects.ts), lines 199–216
|
||||
|
||||
The restore path does **not** call `ensureHomeProvisionSecret`. That call currently exists only when storage is prepared from a fresh login response:
|
||||
|
||||
- [`users.effects.ts`](../toju-app/src/app/store/users/users.effects.ts), lines 288–306
|
||||
|
||||
Provisioning exits when the secret is absent:
|
||||
|
||||
- [`signal-server-auth.service.ts`](../toju-app/src/app/domains/authentication/application/services/signal-server-auth.service.ts), lines 154–168
|
||||
|
||||
The web store uses `sessionStorage`, so a new tab/session can lose the secret even while other persisted identity data remains. Electron storage is durable, but old installs or cleared user data can still have no secret.
|
||||
|
||||
**Required fix:** ensure or deliberately recover the home provision secret before any restore-time or join-time foreign provision. Do not silently swallow restore provisioning errors.
|
||||
|
||||
**Important design caveat:** generating a new secret cannot authenticate foreign accounts created with a lost old secret. The implementation must define recovery for that case instead of looping register/login forever.
|
||||
|
||||
### F2 — Missing foreign credentials block presence and chat fallback
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** The room opens locally, but other users do not see the user; chat is sender-only; voice is empty.
|
||||
|
||||
The room connection refuses to proceed without a credential. The signaling transport skips identify if credentials cannot be resolved. The server rejects all non-keepalive, non-identify messages from unauthenticated connections.
|
||||
|
||||
Live WebSocket channel chat is also membership-gated:
|
||||
|
||||
- [`server/src/websocket/handler.ts`](../server/src/websocket/handler.ts), lines 539–552
|
||||
|
||||
Therefore auth/presence must be repaired before treating chat or voice as isolated transport bugs.
|
||||
|
||||
**Required fix:** prove the complete sequence `credential -> identify(actor id) -> join_server -> mutual roster`, including reconnect.
|
||||
|
||||
### F3 — WebRTC initiator and glare logic use the wrong identity space
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** Cross-home peers connect intermittently, both offer, both wait, or voice remains one-way/connecting.
|
||||
|
||||
Per-signal credentials exist through `getIdentifyCredentialsForSignalUrl`, but the generic getter prefers the home credential:
|
||||
|
||||
- [`signaling-transport-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-transport-handler.ts), lines 35–71
|
||||
|
||||
The peer manager and incoming signaling handler receive the generic home-biased getter:
|
||||
|
||||
- [`realtime-session.service.ts`](../toju-app/src/app/infrastructure/realtime/realtime-session.service.ts), lines 198–208 and 232–234
|
||||
|
||||
Offer collision handling compares that id with the remote signal actor id:
|
||||
|
||||
- [`negotiation.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/connection/negotiation.ts), lines 106–126
|
||||
|
||||
Initiator election performs the same lexical comparison:
|
||||
|
||||
- [`signaling-message-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-message-handler.ts), lines 528–535
|
||||
|
||||
**Required fix:** carry connection/signal scope into initiator election, polite-peer handling, reconnect, and P2P voice-state payloads. Both peers must compare actor ids from the same signal URL.
|
||||
|
||||
### F4 — Reconnect budget is consumed while signaling is unavailable
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** A signal outage lasts about one minute; signaling returns, but the peer mesh does not retry and no error is shown.
|
||||
|
||||
Each timer tick increments `reconnectAttempts` before checking signaling connectivity. At the limit, the timer and tracker are deleted:
|
||||
|
||||
- [`peer-recovery.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/recovery/peer-recovery.ts), lines 282–321
|
||||
|
||||
This means the client can spend all 12 attempts doing no actual reconnection work.
|
||||
|
||||
**Required fix:** do not consume an attempt until an offer/reconnect is actually attempted. On exhaustion, publish an explicit failed state with a user-visible Retry action. A later presence or signaling recovery event must be able to re-arm repair.
|
||||
|
||||
### F5 — A closed data channel tears down media
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** Voice, camera, or screen share drops when chat/file control transport fails.
|
||||
|
||||
The active recovery path calls `removePeer`, then creates a new peer transport:
|
||||
|
||||
- [`peer-recovery.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/recovery/peer-recovery.ts), lines 208–235
|
||||
|
||||
The realtime README claims the initiator creates a replacement data channel on the existing `RTCPeerConnection`:
|
||||
|
||||
- [`realtime/README.md`](../toju-app/src/app/infrastructure/realtime/README.md), line 263
|
||||
|
||||
That claim is false for the current recovery path. `replaceDataChannel` exists but is not the implemented repair.
|
||||
|
||||
**Required decision:**
|
||||
|
||||
- **Recommended emergency step:** keep full rebuild initially, but force resync, preserve/reapply media state, expose progress/failure, and fix the docs.
|
||||
- **Follow-up reliability step:** implement a true soft data-channel replacement on a healthy peer connection, with full rebuild as fallback.
|
||||
|
||||
### F6 — Direct messages can fork by home id versus actor id
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** Two threads represent the same person; a reply lands in the thread the sender is not viewing.
|
||||
|
||||
The pair id is a literal sorted pair of supplied ids:
|
||||
|
||||
- [`direct-message.logic.ts`](../toju-app/src/app/domains/direct-message/domain/logic/direct-message.logic.ts), lines 17–21
|
||||
|
||||
Inbound handling trusts `payload.message.conversationId` before deriving a local id and performs no alias merge:
|
||||
|
||||
- [`direct-message.service.ts`](../toju-app/src/app/domains/direct-message/application/services/direct-message.service.ts), lines 511–541
|
||||
|
||||
The lesson symbols `resolveDirectConversationId` and `mergeAliasDirectConversations` do not exist in the product tree.
|
||||
|
||||
**Required fix:** define a canonical local human identity, remap inbound actor ids, merge duplicate conversations transactionally, and preserve messages/unread/status ordering.
|
||||
|
||||
### F7 — Outbound private calls can fail silently after local join
|
||||
|
||||
**Severity:** P0
|
||||
**User symptom:** Caller sees “In Voice”; callee never rings.
|
||||
|
||||
`startCall` joins the call locally before sending the ring:
|
||||
|
||||
- [`direct-call.service.ts`](../toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts), lines 220–235
|
||||
|
||||
`sendCallEvent` does not await or inspect delivery:
|
||||
|
||||
- [`direct-call.service.ts`](../toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts), lines 759–773
|
||||
|
||||
Candidate expansion in `PeerDeliveryService` helps when a matching NgRx user alias exists, but there is no complete credential/presence-based routable-id selection and no unreachable UX.
|
||||
|
||||
**Required fix:** resolve the connected actor id before committing the caller to the session, return a delivery result, and transition to `recipientUnreachable`/Retry when no route accepts the ring.
|
||||
|
||||
### F8 — Message synchronization can falsely become “clean”
|
||||
|
||||
**Severity:** P1, potentially P0 for long missing-history windows
|
||||
**User symptom:** Live chat works, but old messages remain missing for up to 15 minutes.
|
||||
|
||||
Periodic sync sends inventory requests and dispatches `startSync`:
|
||||
|
||||
- [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 166–213
|
||||
|
||||
Five seconds later, if the reducer still says syncing, the effect unconditionally sets `lastSyncClean = true` and dispatches `syncComplete`:
|
||||
|
||||
- [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 215–232
|
||||
|
||||
No evidence of an empty inventory result is required. The next interval can therefore change from 10 seconds to 15 minutes after timeout rather than convergence.
|
||||
|
||||
**Required fix:** model an inventory round explicitly. Mark clean only when all expected peers have replied and no missing ids remain. Timeouts and newly received messages must remain dirty.
|
||||
|
||||
### F9 — Live chat fallback and history repair are separate
|
||||
|
||||
**Severity:** P1
|
||||
**User symptom:** New text arrives while older text, edits, files, and emoji remain absent.
|
||||
|
||||
The WebSocket `chat_message` path handles live channel messages for joined members. Inventory and sync batches are data-channel-only. `message-revision` has no equivalent WebSocket fallback.
|
||||
|
||||
`onPeerConnected` kicks inventory:
|
||||
|
||||
- [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 64–94
|
||||
|
||||
That signal can occur at peer-connection state `connected` before the data channel is open; a second data-channel-open signal usually retries, but there is no explicit durable “control channel reopened and inventory completed” state.
|
||||
|
||||
**Required fix:** trigger and track reconciliation from data-channel open, not only generic peer connected. Queue failed inventory sends and retry on channel open.
|
||||
|
||||
### F10 — STUN-only defaults cannot guarantee Discord-like reachability
|
||||
|
||||
**Severity:** P1 product limitation
|
||||
**User symptom:** Voice works on friendly networks but fails on symmetric NAT, enterprise Wi-Fi, carrier networks, or restrictive firewalls.
|
||||
|
||||
TURN can be configured by the user, but no managed/default TURN service is supplied. Localhost E2E cannot validate NAT traversal.
|
||||
|
||||
**Required product decision:** operate a credentialed TURN service with expiry/abuse controls, or explicitly reject the requirement that voice “always works.” Documentation alone does not satisfy the stated target.
|
||||
|
||||
## Claims that should not be treated as active defects
|
||||
|
||||
### Identify cache fallback — mitigated
|
||||
|
||||
`getIdentifyCredentialsForSignalUrl` falls back to the credential store when the per-URL cache is empty:
|
||||
|
||||
- [`signaling-transport-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-transport-handler.ts), lines 47–71
|
||||
|
||||
Keep this as a regression invariant. Add a focused spec; do not rewrite this path without a failing reproduction.
|
||||
|
||||
### Join-before-identify race — substantially mitigated
|
||||
|
||||
Client reconnect sends identify before rejoin, and the server serializes message handling per connection. The remaining P0 case is missing credentials, not evidence that server serialization is currently broken.
|
||||
|
||||
### Shared `clientInstanceId` across tabs — fixed
|
||||
|
||||
Current code uses `sessionStorage` and clears the legacy local-storage value. Retain the existing regression test.
|
||||
|
||||
### Multi-signal `user_left` teardown — mitigated
|
||||
|
||||
Current client/server paths carry server membership information and preserve peers still shared elsewhere. This needs multi-signal integration coverage, but the pack should not call it a confirmed current failure.
|
||||
|
||||
### Voice allow-list alias miss — partial
|
||||
|
||||
The voice playback service already collects `id`, `oderId`, and `peerId`. More likely active failures are stale/missing `voiceState`, a peer key absent from all three aliases, or the broader home/actor negotiation mismatch. Instrument before changing routing filters.
|
||||
|
||||
### “No retry after abandon” — overstated
|
||||
|
||||
Presence fallback timers can create a later offer. The confirmed defect is that the explicit reconnect tracker is silently discarded and signaling-down ticks consume its budget.
|
||||
|
||||
### Attachment announce/message race — fixed, retain
|
||||
|
||||
The incoming message handler re-queues auto-download after binding message to room, with unit coverage. Add reorder/large-file E2E rather than reimplementing the fix.
|
||||
|
||||
## E2E baseline assessment
|
||||
|
||||
The archived run is internally consistent:
|
||||
|
||||
- 65 passed
|
||||
- 0 failed
|
||||
- 0 flaky
|
||||
- 0 skipped
|
||||
- about 13.4 minutes
|
||||
|
||||
Sources:
|
||||
|
||||
- [`e2e-failures/README.md`](e2e-failures/README.md)
|
||||
- [`e2e-failures/00-summary.md`](e2e-failures/00-summary.md)
|
||||
- `e2e-failures/parsed-report.json`
|
||||
- `e2e-failures/full-run.log`
|
||||
|
||||
### What it proves
|
||||
|
||||
- Scripted flows passed once in Chromium with retries disabled.
|
||||
- The Angular development server, ephemeral localhost signaling servers, fake mic/camera, and synthetic screen capture can support the covered scenarios.
|
||||
- Strong existing scenarios include two-user voice, eight-user local multi-signal voice, synthetic data-channel recovery, direct-call answer/audio on a same-route setup, chat sync, and several attachment regressions.
|
||||
|
||||
### What it does not prove
|
||||
|
||||
The Playwright configuration runs only Chromium against `ng serve`:
|
||||
|
||||
- [`e2e/playwright.config.ts`](../e2e/playwright.config.ts), lines 3–39
|
||||
|
||||
It does not run Electron, a packaged production build, real desktop safeStorage/SQLite behavior, real screen capture, real TURN/NAT, sleep/wake, or a production signal deployment.
|
||||
|
||||
The cross-signal auth test adds the second endpoint manually before checking provision:
|
||||
|
||||
- [`multi-signal-server-auth.spec.ts`](../e2e/tests/auth/multi-signal-server-auth.spec.ts), lines 20–70
|
||||
|
||||
It does not cover restore with a missing secret or joining an unknown foreign invite.
|
||||
|
||||
The cross-signal call test proves only that the callee sees a ring:
|
||||
|
||||
- [`dm-header-call-ring.spec.ts`](../e2e/tests/voice/dm-header-call-ring.spec.ts), lines 92–184
|
||||
|
||||
It does not answer the call, verify audio, exercise people-card home-id routing, or prove one DM thread.
|
||||
|
||||
The offline DM test proves only local `QUEUED` state:
|
||||
|
||||
- [`dm-flow.spec.ts`](../e2e/tests/chat/dm-flow.spec.ts), lines 15–36
|
||||
|
||||
It never reconnects or proves recipient delivery.
|
||||
|
||||
The mesh helper permits fewer than the calculated expected connections:
|
||||
|
||||
- [`signal-manager.ts`](../e2e/helpers/signal-manager.ts), lines 28–50
|
||||
|
||||
This can let a partial dual-signal mesh pass. Debug inspection also depends on Angular's dev-only `window.ng`.
|
||||
|
||||
The data-channel test proves that audio eventually resumes after a synthetic close:
|
||||
|
||||
- [`data-channel-recovery.spec.ts`](../e2e/tests/voice/data-channel-recovery.spec.ts), lines 27–54
|
||||
|
||||
It does not prove uninterrupted audio, which is consistent with the current full peer teardown.
|
||||
|
||||
## Revised implementation order
|
||||
|
||||
Do not tune codecs, ICE timers, or TURN before identity and membership are correct.
|
||||
|
||||
### Packet 0 — Reproduction and observability
|
||||
|
||||
**Goal:** produce one deterministic two-signal matrix and enough correlated data to prove each later fix.
|
||||
|
||||
Log in debug builds:
|
||||
|
||||
- home user id;
|
||||
- credential actor id per normalized signal URL;
|
||||
- room id/source URL;
|
||||
- peer-map key and peer signal URL;
|
||||
- initiator/polite decision inputs;
|
||||
- call `targetUserId` and delivery outcome;
|
||||
- DM incoming and resolved conversation ids;
|
||||
- data-channel generation and reconnect attempt reason.
|
||||
|
||||
Do not log tokens, passwords, provision secrets, SDP bodies, or message contents.
|
||||
|
||||
### Packet 1 — Restore-safe silent foreign auth
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `toju-app/src/app/store/users/users.effects.ts`
|
||||
- `toju-app/src/app/domains/authentication/application/services/signal-server-auth.service.ts`
|
||||
- `signal-server-authorize.service.ts`
|
||||
- provision-secret stores; targeted Electron bridge only if the persistence contract changes
|
||||
|
||||
**Tests:**
|
||||
|
||||
- restored home session + absent secret;
|
||||
- restored Electron session;
|
||||
- web new-tab behavior;
|
||||
- old foreign account whose previous secret is lost;
|
||||
- offline/unknown endpoint never opens authorize;
|
||||
- foreign `auth_error` does not expire the valid home session.
|
||||
|
||||
### Packet 2 — Identify/join/presence integrity
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `room-signaling-connection.ts`
|
||||
- `signaling-transport-handler.ts`
|
||||
- `signaling.manager.ts`
|
||||
- targeted `server/src/websocket/handler.ts` only if a failing test proves a server change is needed
|
||||
|
||||
**Tests:**
|
||||
|
||||
- credential-store fallback on a fresh socket;
|
||||
- identify is first authenticated message before join;
|
||||
- signal-server restart restores both users to the roster;
|
||||
- leaving one signal scope does not remove a peer shared through another.
|
||||
|
||||
### Packet 3 — Cross-signal identity for calls and DMs
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `domains/direct-message/**`
|
||||
- `domains/direct-call/**`
|
||||
- `peer-delivery.service.ts`
|
||||
- authentication credential/alias readers
|
||||
|
||||
**Tests:**
|
||||
|
||||
- candidate collection from home id, actor id, peer id, and known presence routes;
|
||||
- unreachable call never leaves caller silently in voice;
|
||||
- merge two alias conversations without message/status loss;
|
||||
- cross-signal DM reply remains in one thread;
|
||||
- cross-signal call rings, answers, and carries bidirectional audio.
|
||||
|
||||
### Packet 4 — Per-signal WebRTC identity
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `realtime-session.service.ts`
|
||||
- `signaling-message-handler.ts`
|
||||
- `connection/negotiation.ts`
|
||||
- `recovery/peer-recovery.ts`
|
||||
- voice-state data-channel payload construction
|
||||
|
||||
**Tests:**
|
||||
|
||||
- home-id/actor-id matrix elects exactly one initiator;
|
||||
- polite-peer collision decision uses the same scoped ids;
|
||||
- same-home and cross-home voice are bidirectional;
|
||||
- stale voice state cannot leave a connected peer permanently muted without recovery.
|
||||
|
||||
### Packet 5 — Honest data-channel and reconnect recovery
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `recovery/peer-recovery.ts`
|
||||
- `messaging/data-channel.ts`
|
||||
- `peer-connection.manager.ts`
|
||||
- user-visible connectivity state/components
|
||||
|
||||
**Tests:**
|
||||
|
||||
- signaling-down ticks do not consume reconnect attempts;
|
||||
- max real attempts exposes Retry;
|
||||
- Retry re-arms recovery;
|
||||
- data-channel reopen forces inventory;
|
||||
- media state is reapplied after full rebuild;
|
||||
- if soft replacement is implemented, audio remains continuously flowing.
|
||||
|
||||
### Packet 6 — Message convergence and file integrity
|
||||
|
||||
**Primary files:**
|
||||
|
||||
- `store/messages/messages-sync.effects.ts`
|
||||
- message sync reducers/rules/handlers
|
||||
- attachment transfer only where a failing test identifies a gap
|
||||
|
||||
**Tests:**
|
||||
|
||||
- timeout remains dirty;
|
||||
- only complete, empty inventory rounds select the 15-minute cadence;
|
||||
- late join catches up;
|
||||
- edit/delete converge after DC recovery;
|
||||
- offline DM reconnects and delivers;
|
||||
- large-file download SHA-256 and size match the source;
|
||||
- attachment announce/message order is stressed in both channel and DM paths.
|
||||
|
||||
### Packet 7 — Production reliability proof
|
||||
|
||||
Add separate suites rather than weakening existing fast PR coverage:
|
||||
|
||||
- Electron smoke: login restore, foreign provision, text, file, voice;
|
||||
- signal restart during active voice and DM;
|
||||
- TURN-backed relay-only test;
|
||||
- sleep/wake or network-interface-change test;
|
||||
- 30-minute scheduled voice/screen-share soak;
|
||||
- exact mesh assertions where topology guarantees the count.
|
||||
|
||||
## Documentation corrections required with fixes
|
||||
|
||||
1. Replace the false soft-data-channel paragraph in `realtime/README.md` unless soft replacement ships.
|
||||
2. Relabel the LESSONS outbound-call and DM-canonicalization entries as unfinished specifications until real symbols and tests exist.
|
||||
3. Correct authentication README endpoints from `/api/auth/*` to `/api/users/*`.
|
||||
4. Correct chat README inventory cap from 1,000 to 20,000.
|
||||
5. Mark `shared-kernel/signaling-contracts.ts` non-authoritative or align it with live wire names.
|
||||
6. Correct `e2e/CONTEXT.md` if it claims Electron coverage; the current suite is browser + `ng serve`.
|
||||
|
||||
## Decisions required from the product owner before implementation
|
||||
|
||||
1. **Lost provision secret recovery**
|
||||
- Recommended: automatically create a secret when none has ever existed; when an existing foreign account rejects the new secret, show a targeted per-server recovery action instead of a generic login loop.
|
||||
2. **Web secret durability**
|
||||
- Recommended emergency default: keep session-scoped storage for security, but make the new-tab limitation explicit and recover without misclassifying home logout.
|
||||
3. **Data-channel strategy**
|
||||
- Recommended: ship visible full-rebuild recovery + forced resync first, then soft replacement as a measured follow-up.
|
||||
4. **TURN**
|
||||
- Recommended for the stated Discord-quality goal: managed credentialed TURN, not user-only configuration.
|
||||
5. **History availability**
|
||||
- The signal server stores no chat history. Decide whether “offline users always recover history” may depend on another online client, or whether an encrypted durable mailbox/history service is required.
|
||||
6. **Presence semantics**
|
||||
- Define online/idle/DND/offline and stale timeout behavior. Current emergency tests mostly prove connectivity, not Discord-like status semantics.
|
||||
|
||||
## Definition of done for Fable 5
|
||||
|
||||
For every packet:
|
||||
|
||||
1. Start from a behavior-level failing test or deterministic reproduction.
|
||||
2. Change only the packet's owned surfaces.
|
||||
3. Record actor ids and signal scope in test diagnostics without secrets.
|
||||
4. Run focused unit/integration tests, then the relevant Playwright subset.
|
||||
5. Run lint/build for touched packages.
|
||||
6. Update lying docs in the same change.
|
||||
7. Do not mark complete from unit-green alone; attach the user-visible proof.
|
||||
|
||||
The green 65-test archive is a useful baseline, not release certification. Release confidence requires the two-signal cross-home matrix, real restart recovery, Electron coverage, and TURN/NAT validation described above.
|
||||
@@ -1,19 +0,0 @@
|
||||
# E2E summary
|
||||
|
||||
- Run: 2026-08-12T10:58:08Z → 2026-08-12T11:11:34Z UTC
|
||||
- **65 passed / 0 failed / 0 flaky / 0 timeouts**
|
||||
- Wall: ~13.4 min
|
||||
|
||||
## Errors
|
||||
|
||||
None.
|
||||
|
||||
## Soft signals (passed but slow)
|
||||
|
||||
- 128.2s — `voice/mixed-signal-config-voice.spec.ts` — 8 users with different signal configs can voice, mute, deafen, and chat concurrently
|
||||
- 125.1s — `voice/multi-signal-eight-user-voice.spec.ts` — keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds
|
||||
- 53.4s — `auth/user-session-data-isolation.spec.ts` — gives a new user a blank slate and restores only that user local data after account switches
|
||||
- 33.7s — `chat/chat-message-features.spec.ts` — shows per-server channel lists on first saved-server click
|
||||
- 30.3s — `chat/multi-client-chat-sync.spec.ts` — syncs messages between same-user devices and late-joining users after offline gaps
|
||||
|
||||
See `README.md` for full duration table.
|
||||
@@ -1,152 +0,0 @@
|
||||
# E2E Playwright run — emergency-fix baseline
|
||||
|
||||
- **Started (UTC):** 2026-08-12T10:58:08Z
|
||||
- **Finished (UTC):** 2026-08-12T11:11:34Z
|
||||
- **Wall duration:** 804.6s (~13.4 min)
|
||||
- **Command:** `npm run test:e2e -- --reporter=list --reporter=json`
|
||||
- **Result:** 65 passed, 0 failed, 0 flaky, 0 skipped
|
||||
|
||||
## Verdict
|
||||
|
||||
**No hard errors.** The suite completed green: 65/65 expected, 0 unexpected, 0 timeouts, 0 flakes.
|
||||
|
||||
There is nothing to triage as a failing assertion in this run. Slow cases below are the only signals worth watching if “timeouts shouldn’t happen / app should be fast.”
|
||||
|
||||
## Errors / failures
|
||||
|
||||
_None in this run._
|
||||
|
||||
Stale note: `test-results/.last-run.json` and `test-results/html-report/` still show an old July failure; this run overrode reporters with list+json and did not refresh those artifacts.
|
||||
|
||||
## Slow tests (≥30s) — soft concern
|
||||
|
||||
| Duration | File | Test |
|
||||
| --- | --- | --- |
|
||||
| 128.2s | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
|
||||
| 125.1s | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
|
||||
| 53.4s | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
|
||||
| 33.7s | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
|
||||
| 30.3s | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
|
||||
|
||||
These passed, but they are the closest thing to “timeout risk” in a green suite. Multi-user voice specs dominate wall time.
|
||||
|
||||
## Per-file totals
|
||||
|
||||
| File | Tests | Total | Max |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `voice/mixed-signal-config-voice.spec.ts` | 1 | 128.2s | 128.2s |
|
||||
| `voice/multi-signal-eight-user-voice.spec.ts` | 1 | 125.1s | 125.1s |
|
||||
| `chat/chat-message-features.spec.ts` | 9 | 84.2s | 33.7s |
|
||||
| `auth/user-session-data-isolation.spec.ts` | 2 | 73.2s | 53.4s |
|
||||
| `screen-share/screen-share.spec.ts` | 3 | 49.8s | 23.4s |
|
||||
| `voice/direct-call.spec.ts` | 3 | 31.8s | 17.5s |
|
||||
| `chat/multi-client-chat-sync.spec.ts` | 1 | 30.3s | 30.3s |
|
||||
| `plugins/plugin-api-two-users.spec.ts` | 1 | 27.4s | 27.4s |
|
||||
| `chat/notifications.spec.ts` | 2 | 25.6s | 20.9s |
|
||||
| `voice/data-channel-recovery.spec.ts` | 2 | 22.1s | 12.1s |
|
||||
| `chat/profile-avatar-sync.spec.ts` | 2 | 21.5s | 11.9s |
|
||||
| `settings/connectivity-warning.spec.ts` | 1 | 21.3s | 21.3s |
|
||||
| `voice/voice-full-journey.spec.ts` | 1 | 21.1s | 21.1s |
|
||||
| `chat/server-icon-sync.spec.ts` | 1 | 14.2s | 14.2s |
|
||||
| `chat/dm-flow.spec.ts` | 3 | 11.8s | 4.0s |
|
||||
| `settings/stun-turn-fallback.spec.ts` | 1 | 11.5s | 11.5s |
|
||||
| `auth/login-return-url.spec.ts` | 3 | 9.4s | 3.5s |
|
||||
| `voice/dm-header-call-ring.spec.ts` | 2 | 9.1s | 4.8s |
|
||||
| `auth/multi-device-session.spec.ts` | 1 | 8.6s | 8.6s |
|
||||
| `chat/multi-device-attachment-sharing.spec.ts` | 2 | 8.6s | 4.3s |
|
||||
| `voice/voice-mute-state-reset.spec.ts` | 1 | 6.9s | 6.9s |
|
||||
| `servers/server-discovery-default.spec.ts` | 2 | 6.3s | 3.1s |
|
||||
| `settings/ice-server-settings.spec.ts` | 2 | 6.2s | 3.9s |
|
||||
| `chat/large-generic-file-transfer.spec.ts` | 1 | 5.7s | 5.7s |
|
||||
| `plugins/plugin-manager-ui.spec.ts` | 1 | 4.6s | 4.6s |
|
||||
| `chat/custom-emoji-user-binding.spec.ts` | 1 | 3.9s | 3.9s |
|
||||
| `auth/multi-signal-server-auth.spec.ts` | 1 | 3.8s | 3.8s |
|
||||
| `auth/offline-signal-server-no-login-loop.spec.ts` | 1 | 3.7s | 3.7s |
|
||||
| `chat/local-attachment-persistence.spec.ts` | 1 | 2.8s | 2.8s |
|
||||
| `chat/attachment-only-message-grouping.spec.ts` | 1 | 2.5s | 2.5s |
|
||||
| `mobile/mobile-login-on-startup.spec.ts` | 2 | 2.4s | 1.2s |
|
||||
| `chat/multi-image-gallery.spec.ts` | 1 | 2.3s | 2.3s |
|
||||
| `plugins/plugin-support-api.spec.ts` | 1 | 1.9s | 1.9s |
|
||||
| `mobile/mobile-settings-logout.spec.ts` | 1 | 1.7s | 1.7s |
|
||||
| `mobile/android-app-icon.spec.ts` | 6 | 0.0s | 0.0s |
|
||||
|
||||
## All tests by duration
|
||||
|
||||
| Duration | Status | File | Test |
|
||||
| --- | --- | --- | --- |
|
||||
| 128.2s | passed | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
|
||||
| 125.1s | passed | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
|
||||
| 53.4s | passed | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
|
||||
| 33.7s | passed | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
|
||||
| 30.3s | passed | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
|
||||
| 27.4s | passed | `plugins/plugin-api-two-users.spec.ts` | runs chat, embed, soundboard, and profile APIs between two users |
|
||||
| 23.4s | passed | `screen-share/screen-share.spec.ts` | screen share connection stays stable for 10+ seconds |
|
||||
| 21.3s | passed | `settings/connectivity-warning.spec.ts` | shows warning icon when a peer loses all connections |
|
||||
| 21.1s | passed | `voice/voice-full-journey.spec.ts` | two users register, create server, join voice, and stay connected 10+ seconds with audio |
|
||||
| 20.9s | passed | `chat/notifications.spec.ts` | keeps unread badges visible when a muted channel suppresses desktop popups |
|
||||
| 19.7s | passed | `auth/user-session-data-isolation.spec.ts` | preserves a user saved rooms and local history across app restarts |
|
||||
| 19.6s | passed | `chat/chat-message-features.spec.ts` | syncs messages in a newly created text channel |
|
||||
| 17.5s | passed | `voice/direct-call.spec.ts` | two users can ring, answer, chat, see self voice indicators, and exchange audio |
|
||||
| 15.4s | passed | `screen-share/screen-share.spec.ts` | single user screen share: video and audio flow to receiver, voice audio continues |
|
||||
| 14.2s | passed | `chat/server-icon-sync.spec.ts` | loads the chat-server image for online, late-joining, restarted, and discovery users |
|
||||
| 12.1s | passed | `voice/data-channel-recovery.spec.ts` | heals a three-user voice mesh when one client loses every data channel |
|
||||
| 11.9s | passed | `chat/profile-avatar-sync.spec.ts` | syncs display name and description changes for online and late-joining users and persists after restart |
|
||||
| 11.5s | passed | `settings/stun-turn-fallback.spec.ts` | users with different ICE configs can voice chat together |
|
||||
| 11.1s | passed | `screen-share/screen-share.spec.ts` | multiple users screen share simultaneously |
|
||||
| 10.0s | passed | `voice/data-channel-recovery.spec.ts` | keeps two users hearing each other after a data-channel error and close |
|
||||
| 9.6s | passed | `chat/profile-avatar-sync.spec.ts` | syncs avatar changes for online and late-joining users and persists after restart |
|
||||
| 8.9s | passed | `voice/direct-call.spec.ts` | keeps private-call audio flowing after the data channel closes |
|
||||
| 8.6s | passed | `auth/multi-device-session.spec.ts` | covers identity, chat sync, typing exclusion, and voice exclusivity |
|
||||
| 6.9s | passed | `voice/voice-mute-state-reset.spec.ts` | clears stale mute state after abrupt disconnect and voice rejoin |
|
||||
| 5.7s | passed | `chat/large-generic-file-transfer.spec.ts` | browser receiver can request and download a generic file above the auto-save cap |
|
||||
| 5.5s | passed | `chat/chat-message-features.spec.ts` | edits and removes messages for both users |
|
||||
| 5.3s | passed | `voice/direct-call.spec.ts` | missing and ended private calls do not leave stale call controls behind |
|
||||
| 4.8s | passed | `voice/dm-header-call-ring.spec.ts` | callee homed on another signal server is notified when called via their provisioned actor id |
|
||||
| 4.8s | passed | `chat/notifications.spec.ts` | shows desktop notifications and unread badges for inactive channels |
|
||||
| 4.6s | passed | `chat/chat-message-features.spec.ts` | sends KLIPY GIF messages with mocked API responses |
|
||||
| 4.6s | passed | `plugins/plugin-manager-ui.spec.ts` | installs, grants, activates, and logs an all-API test plugin |
|
||||
| 4.4s | passed | `chat/chat-message-features.spec.ts` | syncs image and file attachments between users |
|
||||
| 4.4s | passed | `chat/chat-message-features.spec.ts` | syncs multi-chunk image attachments byte-identical between users |
|
||||
| 4.3s | passed | `chat/multi-device-attachment-sharing.spec.ts` | only the uploading device claims "Shared from your device"; the second same-user device can request it |
|
||||
| 4.3s | passed | `chat/chat-message-features.spec.ts` | renders link embeds for shared links |
|
||||
| 4.3s | passed | `chat/multi-device-attachment-sharing.spec.ts` | relays file-announce metadata to a sibling device that is already online during upload |
|
||||
| 4.2s | passed | `voice/dm-header-call-ring.spec.ts` | callee is notified when the caller starts the call from the DM chat header |
|
||||
| 4.1s | passed | `chat/chat-message-features.spec.ts` | shows typing indicators to other users |
|
||||
| 4.0s | passed | `chat/dm-flow.spec.ts` | delivers a live DM to the recipient conversation |
|
||||
| 3.9s | passed | `chat/custom-emoji-user-binding.spec.ts` | a second user on the same client does not inherit the first user library |
|
||||
| 3.9s | passed | `chat/dm-flow.spec.ts` | opens a DM from a user card and queues messages while offline |
|
||||
| 3.9s | passed | `settings/ice-server-settings.spec.ts` | allows adding, removing, and reordering ICE servers |
|
||||
| 3.8s | passed | `chat/dm-flow.spec.ts` | shows friend and message actions on the search people list |
|
||||
| 3.8s | passed | `auth/multi-signal-server-auth.spec.ts` | auto-provisions a foreign signal server when a new endpoint is added |
|
||||
| 3.7s | passed | `auth/offline-signal-server-no-login-loop.spec.ts` | does not redirect to authorize login after a foreign server goes offline |
|
||||
| 3.6s | passed | `chat/chat-message-features.spec.ts` | shows local room history on first saved-server click |
|
||||
| 3.5s | passed | `auth/login-return-url.spec.ts` | unwraps nested login returnUrl chains after successful login |
|
||||
| 3.1s | passed | `servers/server-discovery-default.spec.ts` | a fresh account sees public servers in Popular Servers without searching |
|
||||
| 3.1s | passed | `servers/server-discovery-default.spec.ts` | discovery falls back to the public listing when featured/trending routes 404 |
|
||||
| 3.1s | passed | `auth/login-return-url.spec.ts` | redirects unauthenticated /servers visits to login and returns there after login |
|
||||
| 2.8s | passed | `auth/login-return-url.spec.ts` | lets a returning user log back in after an expired session redirect |
|
||||
| 2.8s | passed | `chat/local-attachment-persistence.spec.ts` | remembers sent image and file across a page reload with no peer connected |
|
||||
| 2.5s | passed | `chat/attachment-only-message-grouping.spec.ts` | each caption-less attachment keeps its own message bubble and preview |
|
||||
| 2.3s | passed | `settings/ice-server-settings.spec.ts` | validates TURN entries require credentials |
|
||||
| 2.3s | passed | `chat/multi-image-gallery.spec.ts` | groups three images in one message bubble with a visible grid |
|
||||
| 1.9s | passed | `plugins/plugin-support-api.spec.ts` | covers plugin requirement, event, data, and websocket APIs with the fixture plugin |
|
||||
| 1.7s | passed | `mobile/mobile-settings-logout.spec.ts` | exposes logout in the settings menu on mobile viewports |
|
||||
| 1.2s | passed | `mobile/mobile-login-on-startup.spec.ts` | greets a signed-out mobile visitor on /dashboard with the login screen |
|
||||
| 1.2s | passed | `mobile/mobile-login-on-startup.spec.ts` | greets a signed-out mobile visitor on the app root with the login screen |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | renders the brand mark (white cat on a purple disc) in the launcher bitmap |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | renders the splash art as the brand mark centred on a purple field |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | insets the adaptive foreground so launcher masks do not clip the cat face |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | replaces every stock Capacitor placeholder with the brand asset |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | ships a launcher icon and splash for every required density |
|
||||
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | uses the brand purple as the adaptive-icon background |
|
||||
|
||||
## Files in this folder
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `README.md` | This summary |
|
||||
| `00-summary.md` | Short status for handoff/skimming |
|
||||
| `slow-tests.md` | Slow-test detail only |
|
||||
| `parsed-report.json` | Machine-readable extract |
|
||||
| `full-run.log` | Raw Playwright JSON report (+ npm banner) |
|
||||
| `run-started.txt` / `run-finished.txt` | UTC timestamps |
|
||||
File diff suppressed because one or more lines are too long
@@ -1,516 +0,0 @@
|
||||
{
|
||||
"stats": {
|
||||
"startTime": "2026-08-12T10:58:09.830Z",
|
||||
"duration": 804598.4550000001,
|
||||
"expected": 65,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"failures": [],
|
||||
"timeouts": [],
|
||||
"slow": [
|
||||
{
|
||||
"title": "8 users with different signal configs can voice, mute, deafen, and chat concurrently",
|
||||
"file": "voice/mixed-signal-config-voice.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 128215,
|
||||
"error": null,
|
||||
"errors": [],
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds",
|
||||
"file": "voice/multi-signal-eight-user-voice.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 125136,
|
||||
"error": null,
|
||||
"errors": [],
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "gives a new user a blank slate and restores only that user local data after account switches",
|
||||
"file": "auth/user-session-data-isolation.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 53448,
|
||||
"error": null,
|
||||
"errors": [],
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows per-server channel lists on first saved-server click",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 33665,
|
||||
"error": null,
|
||||
"errors": [],
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs messages between same-user devices and late-joining users after offline gaps",
|
||||
"file": "chat/multi-client-chat-sync.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 30290,
|
||||
"error": null,
|
||||
"errors": [],
|
||||
"timeoutHint": false
|
||||
}
|
||||
],
|
||||
"all": [
|
||||
{
|
||||
"title": "unwraps nested login returnUrl chains after successful login",
|
||||
"file": "auth/login-return-url.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3506,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "redirects unauthenticated /servers visits to login and returns there after login",
|
||||
"file": "auth/login-return-url.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3088,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "lets a returning user log back in after an expired session redirect",
|
||||
"file": "auth/login-return-url.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2811,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "covers identity, chat sync, typing exclusion, and voice exclusivity",
|
||||
"file": "auth/multi-device-session.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 8640,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "auto-provisions a foreign signal server when a new endpoint is added",
|
||||
"file": "auth/multi-signal-server-auth.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3836,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "does not redirect to authorize login after a foreign server goes offline",
|
||||
"file": "auth/offline-signal-server-no-login-loop.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3727,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "preserves a user saved rooms and local history across app restarts",
|
||||
"file": "auth/user-session-data-isolation.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 19725,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "gives a new user a blank slate and restores only that user local data after account switches",
|
||||
"file": "auth/user-session-data-isolation.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 53448,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "each caption-less attachment keeps its own message bubble and preview",
|
||||
"file": "chat/attachment-only-message-grouping.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2507,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows per-server channel lists on first saved-server click",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 33665,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows local room history on first saved-server click",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3598,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs messages in a newly created text channel",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 19613,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows typing indicators to other users",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4090,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "edits and removes messages for both users",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 5534,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs image and file attachments between users",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4442,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs multi-chunk image attachments byte-identical between users",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4369,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "renders link embeds for shared links",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4303,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "sends KLIPY GIF messages with mocked API responses",
|
||||
"file": "chat/chat-message-features.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4610,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "a second user on the same client does not inherit the first user library",
|
||||
"file": "chat/custom-emoji-user-binding.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3929,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "opens a DM from a user card and queues messages while offline",
|
||||
"file": "chat/dm-flow.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3882,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "delivers a live DM to the recipient conversation",
|
||||
"file": "chat/dm-flow.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4024,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows friend and message actions on the search people list",
|
||||
"file": "chat/dm-flow.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3849,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "browser receiver can request and download a generic file above the auto-save cap",
|
||||
"file": "chat/large-generic-file-transfer.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 5690,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "remembers sent image and file across a page reload with no peer connected",
|
||||
"file": "chat/local-attachment-persistence.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2793,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs messages between same-user devices and late-joining users after offline gaps",
|
||||
"file": "chat/multi-client-chat-sync.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 30290,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "only the uploading device claims \"Shared from your device\"; the second same-user device can request it",
|
||||
"file": "chat/multi-device-attachment-sharing.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4333,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "relays file-announce metadata to a sibling device that is already online during upload",
|
||||
"file": "chat/multi-device-attachment-sharing.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4266,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "groups three images in one message bubble with a visible grid",
|
||||
"file": "chat/multi-image-gallery.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2294,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows desktop notifications and unread badges for inactive channels",
|
||||
"file": "chat/notifications.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4758,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "keeps unread badges visible when a muted channel suppresses desktop popups",
|
||||
"file": "chat/notifications.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 20875,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs avatar changes for online and late-joining users and persists after restart",
|
||||
"file": "chat/profile-avatar-sync.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 9605,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "syncs display name and description changes for online and late-joining users and persists after restart",
|
||||
"file": "chat/profile-avatar-sync.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 11864,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "loads the chat-server image for online, late-joining, restarted, and discovery users",
|
||||
"file": "chat/server-icon-sync.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 14204,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "ships a launcher icon and splash for every required density",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "replaces every stock Capacitor placeholder with the brand asset",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "uses the brand purple as the adaptive-icon background",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "renders the brand mark (white cat on a purple disc) in the launcher bitmap",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 13,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "renders the splash art as the brand mark centred on a purple field",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 7,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "insets the adaptive foreground so launcher masks do not clip the cat face",
|
||||
"file": "mobile/android-app-icon.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "greets a signed-out mobile visitor on /dashboard with the login screen",
|
||||
"file": "mobile/mobile-login-on-startup.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1217,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "greets a signed-out mobile visitor on the app root with the login screen",
|
||||
"file": "mobile/mobile-login-on-startup.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1181,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "exposes logout in the settings menu on mobile viewports",
|
||||
"file": "mobile/mobile-settings-logout.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1704,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "runs chat, embed, soundboard, and profile APIs between two users",
|
||||
"file": "plugins/plugin-api-two-users.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 27382,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "installs, grants, activates, and logs an all-API test plugin",
|
||||
"file": "plugins/plugin-manager-ui.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4564,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "covers plugin requirement, event, data, and websocket APIs with the fixture plugin",
|
||||
"file": "plugins/plugin-support-api.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 1913,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "single user screen share: video and audio flow to receiver, voice audio continues",
|
||||
"file": "screen-share/screen-share.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 15375,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "multiple users screen share simultaneously",
|
||||
"file": "screen-share/screen-share.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 11093,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "screen share connection stays stable for 10+ seconds",
|
||||
"file": "screen-share/screen-share.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 23371,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "a fresh account sees public servers in Popular Servers without searching",
|
||||
"file": "servers/server-discovery-default.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3149,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "discovery falls back to the public listing when featured/trending routes 404",
|
||||
"file": "servers/server-discovery-default.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3110,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "shows warning icon when a peer loses all connections",
|
||||
"file": "settings/connectivity-warning.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 21313,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "allows adding, removing, and reordering ICE servers",
|
||||
"file": "settings/ice-server-settings.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 3855,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "validates TURN entries require credentials",
|
||||
"file": "settings/ice-server-settings.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 2311,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "users with different ICE configs can voice chat together",
|
||||
"file": "settings/stun-turn-fallback.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 11549,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "keeps two users hearing each other after a data-channel error and close",
|
||||
"file": "voice/data-channel-recovery.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 10035,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "heals a three-user voice mesh when one client loses every data channel",
|
||||
"file": "voice/data-channel-recovery.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 12070,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "two users can ring, answer, chat, see self voice indicators, and exchange audio",
|
||||
"file": "voice/direct-call.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 17480,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "keeps private-call audio flowing after the data channel closes",
|
||||
"file": "voice/direct-call.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 8950,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "missing and ended private calls do not leave stale call controls behind",
|
||||
"file": "voice/direct-call.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 5345,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "callee is notified when the caller starts the call from the DM chat header",
|
||||
"file": "voice/dm-header-call-ring.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4235,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "callee homed on another signal server is notified when called via their provisioned actor id",
|
||||
"file": "voice/dm-header-call-ring.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 4815,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "8 users with different signal configs can voice, mute, deafen, and chat concurrently",
|
||||
"file": "voice/mixed-signal-config-voice.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 128215,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds",
|
||||
"file": "voice/multi-signal-eight-user-voice.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 125136,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "two users register, create server, join voice, and stay connected 10+ seconds with audio",
|
||||
"file": "voice/voice-full-journey.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 21107,
|
||||
"timeoutHint": false
|
||||
},
|
||||
{
|
||||
"title": "clears stale mute state after abrupt disconnect and voice rejoin",
|
||||
"file": "voice/voice-mute-state-reset.spec.ts",
|
||||
"status": "passed",
|
||||
"duration": 6888,
|
||||
"timeoutHint": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2026-08-12T11:11:34Z
|
||||
@@ -1 +0,0 @@
|
||||
2026-08-12T10:58:08Z
|
||||
@@ -1,17 +0,0 @@
|
||||
# Slow E2E tests (≥30s)
|
||||
|
||||
All of these **passed**. Listed because the product ask is that timeouts should not happen and the app should stay fast.
|
||||
|
||||
| Duration | File | Test |
|
||||
| --- | --- | --- |
|
||||
| 128.2s | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
|
||||
| 125.1s | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
|
||||
| 53.4s | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
|
||||
| 33.7s | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
|
||||
| 30.3s | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
|
||||
|
||||
## Notes
|
||||
|
||||
- `mixed-signal-config-voice` and `multi-signal-eight-user-voice` are intentionally heavy (8 users / multi-signal). ~2 minutes each is suite design cost more than a single-user app hang.
|
||||
- `user-session-data-isolation` (~53s) and channel/chat sync (~30–34s) are closer to “app feels slow” candidates if they regress upward.
|
||||
- Playwright config `timeout` is 90s per test; none of the failures/timeouts hit that ceiling in this run.
|
||||
@@ -9,6 +9,16 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Reading IIS configuration (redirection.config) and creating sites needs a full
|
||||
# administrator token. An interactive logon gets a UAC-filtered one, which fails
|
||||
# later with a confusing "cannot retrieve the dynamic parameters" error.
|
||||
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$currentPrincipal = [Security.Principal.WindowsPrincipal]::new($currentIdentity)
|
||||
|
||||
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw "IIS deployment requires an elevated administrator token, but '$($currentIdentity.Name)' does not have one. Add the runner account to the local Administrators group and run the Gitea runner as a Windows service."
|
||||
}
|
||||
|
||||
try {
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user