From 29f83df53c04233d5f8b41fe85e1b87682a73082 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Mon, 28 Sep 2026 23:55:43 +0000 Subject: [PATCH 01/12] chore: bump cinny submodule to c0c93213 --- cinny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinny b/cinny index be8e49a..c0c9321 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit be8e49a2bb52ca6649f5ce88b1a45372900a14b1 +Subproject commit c0c93213c1d8fff41991306f4b1341701722b635 From bf5b6d7b4512634d84e155ac47b825413d372233 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 02:18:04 +0000 Subject: [PATCH 02/12] chore: bump cinny submodule to 899e160a --- cinny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinny b/cinny index c0c9321..899e160 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit c0c93213c1d8fff41991306f4b1341701722b635 +Subproject commit 899e160aedad8cff8caad0f97b015473a84c74d0 From 884877a55b20f364dc23d155e5d3fa42385b50d5 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 04:01:40 +0000 Subject: [PATCH 03/12] chore: bump cinny submodule to 91f82d60 (nightly catch-up) --- cinny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinny b/cinny index 899e160..91f82d6 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 899e160aedad8cff8caad0f97b015473a84c74d0 +Subproject commit 91f82d60e36e295d69f29aaa44a461e83aeca4d5 From 32a71ebb7307b2c79a1fe48d5ef508de7631e4c7 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 13:38:27 +0000 Subject: [PATCH 04/12] chore: bump cinny submodule to d6548c56 --- cinny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinny b/cinny index 91f82d6..d6548c5 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 91f82d60e36e295d69f29aaa44a461e83aeca4d5 +Subproject commit d6548c56fdeb0e8cba2a235007b50ac85c54fb8c From 3cd429d45c136ff486004aa88ba404366fdbbf60 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 12:27:46 -0400 Subject: [PATCH 05/12] config: homeserver status banner from Kuma (cinny #124) Adds the same `statusPages` entry the web app uses, so the desktop app shows the status banner for matrix.lotusguild.org too (Kuma status page https://isitup.lotusguild.org/status/matrix). The desktop CSP already allows https: connections; no other change is needed. Inert until the bundled cinny includes the banner (cinny PR #255). Tested on a Linux release build: with a local fake Kuma reporting calls down, the desktop app shows "Voice calls are down right now. Messages still work." and polls both endpoints; from inside the desktop app the real Kuma page answers 200 on both (CSP and CORS allow it). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- config.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index d524158..4fa9e5d 100644 --- a/config.json +++ b/config.json @@ -24,5 +24,16 @@ "basename": "/" }, "gifApiKey": "", - "webAppUrl": "https://chat.lotusguild.org" + "webAppUrl": "https://chat.lotusguild.org", + "statusPages": { + "matrix.lotusguild.org": { + "url": "https://isitup.lotusguild.org", + "slug": "matrix", + "groups": { + "homeserver": "Homeserver", + "calls": "Voice calls", + "login": "Login" + } + } + } } From 0d86f199356d1262c6dbc7dfeafb35fac0f552c6 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 13:04:41 -0400 Subject: [PATCH 06/12] ci: Windows smoke test on the windows runner (#19) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/windows-smoke.yml | 120 ++++++++++++++++++++ scripts/windows-smoke.mjs | 175 +++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 .gitea/workflows/windows-smoke.yml create mode 100644 scripts/windows-smoke.mjs diff --git a/.gitea/workflows/windows-smoke.yml b/.gitea/workflows/windows-smoke.yml new file mode 100644 index 0000000..ea1546b --- /dev/null +++ b/.gitea/workflows/windows-smoke.yml @@ -0,0 +1,120 @@ +# cinny-desktop #19: smoke-test the Windows app on the `windows` runner. +# +# Installs the NSIS bundle silently, starts the installed app with WebView2's +# DevTools port open, and drives its real page with scripts/windows-smoke.mjs +# (playwright-core over CDP): boots to the login screen, local server address, +# microphone permission for the app and not for a foreign page, the call page's +# own origin + isolation, the Credential Manager round trip. Features a build +# doesn't have are reported "n/a". +# +# Run it from the Actions tab (workflow_dispatch): +# - no `ref`: tests the published nightly installer (a couple of minutes); +# - `ref` = a branch (e.g. a PR branch): builds that branch, then tests it. +name: Windows smoke + +on: + workflow_dispatch: + inputs: + ref: + description: 'Branch to build and test (empty: test the published nightly)' + required: false + default: '' + +env: + GITEA_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + +jobs: + smoke: + runs-on: windows + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Download the published nightly installer + if: ${{ inputs.ref == '' }} + shell: powershell + run: | + New-Item -ItemType Directory -Force -Path smoke-installer | Out-Null + Invoke-WebRequest -Uri "$env:GITEA_URL/$env:REPO/releases/download/latest/LotusChat-x86_64-setup.exe" -OutFile smoke-installer\setup.exe + Get-Item smoke-installer\setup.exe | Select-Object Name, Length + + - name: Check out the branch to build + if: ${{ inputs.ref != '' }} + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + path: build-src + + - name: Build the branch + if: ${{ inputs.ref != '' }} + shell: powershell + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' + NODE_OPTIONS: '--max_old_space_size=4096' + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + CARGO_HTTP_MULTIPLEXING: 'false' + CARGO_NET_RETRY: '5' + run: | + cd build-src + git submodule update --init --depth=1 + cd cinny; npm ci; cd .. + node scripts/sync-web-config.mjs + npm ci + $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" + $toolchain = Get-ChildItem "$env:USERPROFILE\.rustup\toolchains" -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match 'stable' } | Select-Object -First 1 + if ($toolchain) { $env:PATH = "$($toolchain.FullName)\bin;$env:PATH" } + npm run tauri -- build --bundles nsis + New-Item -ItemType Directory -Force -Path ..\smoke-installer | Out-Null + $exe = Get-ChildItem src-tauri\target\release\bundle\nsis\*-setup.exe | Select-Object -First 1 + Copy-Item $exe.FullName ..\smoke-installer\setup.exe + + - name: Install silently + shell: powershell + run: | + Get-Process cinny -ErrorAction SilentlyContinue | Stop-Process -Force + Start-Process smoke-installer\setup.exe -ArgumentList '/S' -Wait + $app = Join-Path $env:LOCALAPPDATA 'Lotus Chat\cinny.exe' + if (-not (Test-Path $app)) { Write-Error "not installed at $app"; exit 1 } + (Get-Item $app).VersionInfo | Select-Object ProductVersion, FileVersion + + - name: Start the app with the WebView2 DevTools port open + shell: powershell + run: | + $env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9222 --use-fake-device-for-media-stream' + $app = Join-Path $env:LOCALAPPDATA 'Lotus Chat\cinny.exe' + Start-Process $app + Start-Sleep -Seconds 5 + Get-Process cinny | Select-Object Id, SessionId, MainWindowTitle + + - name: Smoke test + shell: powershell + run: | + New-Item -ItemType Directory -Force -Path smoke-deps | Out-Null + Push-Location smoke-deps + npm init -y | Out-Null + npm install --no-audit --no-fund playwright-core@1 | Out-Null + Pop-Location + $env:NODE_PATH = (Resolve-Path smoke-deps\node_modules).Path + Copy-Item scripts\windows-smoke.mjs smoke-deps\windows-smoke.mjs + node smoke-deps\windows-smoke.mjs smoke-out + + - name: Stop the app + if: ${{ always() }} + shell: powershell + run: | + Get-Process cinny -ErrorAction SilentlyContinue | Stop-Process -Force + if (Test-Path smoke-out\results.json) { Get-Content smoke-out\results.json } + + - name: Upload results and screenshots + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: windows-smoke + path: smoke-out diff --git a/scripts/windows-smoke.mjs b/scripts/windows-smoke.mjs new file mode 100644 index 0000000..a757340 --- /dev/null +++ b/scripts/windows-smoke.mjs @@ -0,0 +1,175 @@ +// cinny-desktop #19: Windows smoke test against the INSTALLED app. +// +// The app is started with WebView2's DevTools port open +// (WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222) and this +// script drives its real page over CDP with playwright-core. No login: every +// check runs on the login screen or through the app's own Tauri commands. +// +// node scripts/windows-smoke.mjs +// +// Checks whose feature isn't in the build under test are reported "n/a" +// instead of failing, so the same script runs on main and on PR branches. +import { writeFileSync, mkdirSync } from 'node:fs'; +import { execSync } from 'node:child_process'; +import { chromium } from 'playwright-core'; + +const OUT = process.argv[2] || 'smoke-out'; +mkdirSync(OUT, { recursive: true }); +const APP = 'http://localhost:44548'; +const results = []; +const record = (name, status, detail = '') => { + results.push({ name, status, detail }); + console.log(`${status.toUpperCase().padEnd(4)} ${name}${detail ? ` — ${detail}` : ''}`); +}; + +const until = async (fn, ms, step = 500) => { + const end = Date.now() + ms; + for (;;) { + let v; + try { + v = await fn(); + } catch { + v = undefined; + } + if (v || Date.now() > end) return v; + await new Promise((r) => setTimeout(r, step)); + } +}; + +// 1. Connect to the running app's WebView2. +const browser = await until(() => chromium.connectOverCDP('http://127.0.0.1:9222'), 60_000, 1000); +if (!browser) { + record('connect to the app over CDP', 'fail', 'no DevTools endpoint on :9222'); + writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2)); + process.exit(1); +} +const page = await until( + () => browser.contexts().flatMap((c) => c.pages()).find((p) => p.url().startsWith(APP)), + 60_000, +); +if (!page) { + record('app page found', 'fail', browser.contexts().flatMap((c) => c.pages()).map((p) => p.url()).join(', ')); + process.exit(1); +} +record('app page found', 'pass', page.url()); + +// 2. Boots to the login screen. +const booted = await until(async () => /Login|Homeserver/.test(await page.locator('body').innerText()), 60_000); +record('boots to the login screen', booted ? 'pass' : 'fail'); +await page.screenshot({ path: `${OUT}/01-login.png` }); + +// 3. The local server's address (cinny #43 binds 127.0.0.1 explicitly). +try { + const listen = execSync( + 'powershell -NoProfile -Command "(Get-NetTCPConnection -LocalPort 44548 -State Listen).LocalAddress -join \',\'"', + ) + .toString() + .trim(); + record('local server listening', listen ? 'pass' : 'fail', listen); +} catch (e) { + record('local server listening', 'fail', String(e).slice(0, 120)); +} + +const cfg = await page.evaluate(() => fetch('/config.json').then((r) => r.json())); + +// 4. Microphone for the app itself (WebView2 PermissionRequested handler, #22). +const appMic = await page.evaluate(async () => { + try { + const s = await navigator.mediaDevices.getUserMedia({ audio: true }); + s.getTracks().forEach((t) => t.stop()); + return 'ok'; + } catch (e) { + return `${e.name}: ${e.message}`; + } +}); +record('microphone allowed for the app', appMic === 'ok' ? 'pass' : 'fail', appMic); + +// 5. Call page on its own origin, isolated from the app (cinny #43 / #27). +if (cfg.desktopCallOrigin) { + const src = `${cfg.desktopCallOrigin}/public/element-call/index.html`; + await page.evaluate((s) => { + const f = document.createElement('iframe'); + f.id = 'smoke-call'; + f.src = s; + f.allow = 'microphone; camera; display-capture; autoplay; clipboard-write;'; + f.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups allow-modals allow-downloads'; + document.body.appendChild(f); + }, src); + const frame = await until(() => page.frames().find((f) => f.url().startsWith(src)), 30_000); + if (!frame) { + record('call page loads from its own origin', 'fail', 'frame did not load (CSP?)'); + } else { + await frame.waitForLoadState('domcontentloaded').catch(() => undefined); + const iso = await frame.evaluate(async () => { + const r = { origin: location.origin }; + try { + r.parentStorage = String(parent.localStorage.length); + } catch (e) { + r.parentStorage = e.name; + } + try { + const s = await navigator.mediaDevices.getUserMedia({ audio: true }); + s.getTracks().forEach((t) => t.stop()); + r.mic = 'ok'; + } catch (e) { + r.mic = e.name; + } + return r; + }); + record('call page loads from its own origin', 'pass', iso.origin); + record('call page cannot read the app storage', iso.parentStorage === 'SecurityError' ? 'pass' : 'fail', iso.parentStorage); + record('call page gets the microphone', iso.mic === 'ok' ? 'pass' : 'fail', iso.mic); + } +} else { + record('call page on its own origin', 'n/a', 'desktopCallOrigin not set in this build'); +} + +// 6. OS keychain round trip (cinny #105, step 1). +const kc = await page.evaluate(async () => { + const inv = window.__TAURI_INTERNALS__?.invoke; + if (!inv) return { error: 'no Tauri bridge' }; + try { + const supported = await inv('secure_session_supported'); + if (!supported) return { supported }; + const tokens = { userId: '@smoke:ci.invalid', deviceId: 'SMOKE', accessToken: `smoke-${Date.now()}` }; + const before = await inv('secure_session_get'); + await inv('secure_session_set', { tokens }); + const back = await inv('secure_session_get'); + // Put back whatever was there (nothing, on a clean runner). + if (before) await inv('secure_session_set', { tokens: before }); + else await inv('secure_session_clear'); + const after = await inv('secure_session_get'); + return { supported, roundTrip: back?.accessToken === tokens.accessToken, restored: JSON.stringify(after) === JSON.stringify(before ?? null) }; + } catch (e) { + return { error: String(e).slice(0, 160) }; + } +}); +if (kc.error && /not found/i.test(kc.error)) record('keychain round trip', 'n/a', 'commands not in this build'); +else if (kc.error) record('keychain round trip', 'fail', kc.error); +else if (!kc.supported) record('keychain round trip', 'fail', 'secure_session_supported = false on Windows'); +else record('keychain round trip', kc.roundTrip && kc.restored ? 'pass' : 'fail', JSON.stringify(kc)); + +// 7. A foreign page loaded in the window must not get the microphone (#22). +await page.goto('https://example.com/').catch(() => undefined); +const foreignMic = await page.evaluate(async () => { + try { + const s = await navigator.mediaDevices.getUserMedia({ audio: true }); + s.getTracks().forEach((t) => t.stop()); + return 'ok'; + } catch (e) { + return e.name; + } +}); +record( + 'microphone refused to a foreign page', + foreignMic === 'NotAllowedError' ? 'pass' : 'info', + `${foreignMic}${foreignMic === 'ok' ? ' (build without the #22 origin check)' : ''}`, +); +await page.goto(APP).catch(() => undefined); +await page.screenshot({ path: `${OUT}/02-end.png` }).catch(() => undefined); + +writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2)); +await browser.close().catch(() => undefined); +const failed = results.filter((r) => r.status === 'fail'); +console.log(`\n${results.filter((r) => r.status === 'pass').length} passed, ${failed.length} failed`); +process.exit(failed.length ? 1 : 0); From a754bb0a9310d9e333c7cf8068500ce480d31240 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 13:09:26 -0400 Subject: [PATCH 07/12] ci(windows-smoke): start the app in the test step; log processes/ports Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/windows-smoke.yml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/windows-smoke.yml b/.gitea/workflows/windows-smoke.yml index ea1546b..8b975e9 100644 --- a/.gitea/workflows/windows-smoke.yml +++ b/.gitea/workflows/windows-smoke.yml @@ -84,16 +84,7 @@ jobs: if (-not (Test-Path $app)) { Write-Error "not installed at $app"; exit 1 } (Get-Item $app).VersionInfo | Select-Object ProductVersion, FileVersion - - name: Start the app with the WebView2 DevTools port open - shell: powershell - run: | - $env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9222 --use-fake-device-for-media-stream' - $app = Join-Path $env:LOCALAPPDATA 'Lotus Chat\cinny.exe' - Start-Process $app - Start-Sleep -Seconds 5 - Get-Process cinny | Select-Object Id, SessionId, MainWindowTitle - - - name: Smoke test + - name: Start the app and smoke test it shell: powershell run: | New-Item -ItemType Directory -Force -Path smoke-deps | Out-Null @@ -101,8 +92,20 @@ jobs: npm init -y | Out-Null npm install --no-audit --no-fund playwright-core@1 | Out-Null Pop-Location - $env:NODE_PATH = (Resolve-Path smoke-deps\node_modules).Path Copy-Item scripts\windows-smoke.mjs smoke-deps\windows-smoke.mjs + # Same step as the test: a process started in an earlier step may be + # cleaned up when that step ends. + $env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9222 --use-fake-device-for-media-stream' + $app = Join-Path $env:LOCALAPPDATA 'Lotus Chat\cinny.exe' + Start-Process $app + Start-Sleep -Seconds 15 + Write-Host "--- processes" + Get-Process cinny, msedgewebview2 -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId | Format-Table | Out-String | Write-Host + Write-Host "--- listening ports 9222/44548" + Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -in 9222, 44548 } | Select-Object LocalAddress, LocalPort, OwningProcess | Format-Table | Out-String | Write-Host + Write-Host "--- WebView2 runtime" + Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' -ErrorAction SilentlyContinue | Select-Object pv | Out-String | Write-Host + Write-Host "--- whoami: $(whoami)" node smoke-deps\windows-smoke.mjs smoke-out - name: Stop the app From 90c007a95efebb113ed8fd2cbc703076a45050c5 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 13:14:13 -0400 Subject: [PATCH 08/12] ci(windows-smoke): log WebView2 args, netstat and port probes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/windows-smoke.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitea/workflows/windows-smoke.yml b/.gitea/workflows/windows-smoke.yml index 8b975e9..5c145ee 100644 --- a/.gitea/workflows/windows-smoke.yml +++ b/.gitea/workflows/windows-smoke.yml @@ -106,6 +106,13 @@ jobs: Write-Host "--- WebView2 runtime" Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' -ErrorAction SilentlyContinue | Select-Object pv | Out-String | Write-Host Write-Host "--- whoami: $(whoami)" + Write-Host "--- webview2 command lines" + Get-CimInstance Win32_Process -Filter "Name='msedgewebview2.exe'" | Select-Object -First 2 | ForEach-Object { Write-Host $_.CommandLine.Substring(0, [Math]::Min(600, $_.CommandLine.Length)) } + Write-Host "--- netstat" + netstat -ano | Select-String "LISTENING" | Select-String ":9222 |:44548 " | ForEach-Object { Write-Host $_ } + Write-Host "--- http probes" + try { (Invoke-WebRequest -UseBasicParsing http://127.0.0.1:9222/json/version -TimeoutSec 5).Content | Write-Host } catch { Write-Host "9222: $($_.Exception.Message)" } + try { (Invoke-WebRequest -UseBasicParsing http://localhost:44548/ -TimeoutSec 5).StatusCode | Write-Host } catch { Write-Host "44548: $($_.Exception.Message)" } node smoke-deps\windows-smoke.mjs smoke-out - name: Stop the app From 9f0f782bbb30f5002b560e051ebb1eb0b64d7d94 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Tue, 29 Sep 2026 13:19:21 -0400 Subject: [PATCH 09/12] windows smoke: opt-in WebView2 DevTools port via LOTUS_WEBVIEW2_DEBUG_PORT (#19) WebView2's WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS is ignored because the app sets its browser arguments explicitly (seen on the runner: the WebView2 command line had only the app's arguments). The app now appends --remote-debugging-port only when LOTUS_WEBVIEW2_DEBUG_PORT holds a valid port (>= 1024); otherwise the arguments are exactly as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/windows-smoke.yml | 4 ++- src-tauri/src/lib.rs | 44 ++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/windows-smoke.yml b/.gitea/workflows/windows-smoke.yml index 5c145ee..ccd0d5f 100644 --- a/.gitea/workflows/windows-smoke.yml +++ b/.gitea/workflows/windows-smoke.yml @@ -95,7 +95,9 @@ jobs: Copy-Item scripts\windows-smoke.mjs smoke-deps\windows-smoke.mjs # Same step as the test: a process started in an earlier step may be # cleaned up when that step ends. - $env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9222 --use-fake-device-for-media-stream' + # The app sets its own WebView2 arguments, so WebView2's env var doesn't + # apply; the app opens the DevTools port itself when asked (#19). + $env:LOTUS_WEBVIEW2_DEBUG_PORT = '9222' $app = Join-Path $env:LOCALAPPDATA 'Lotus Chat\cinny.exe' Start-Process $app Start-Sleep -Seconds 15 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8cad3b2..87944b3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1128,9 +1128,9 @@ pub fn run() { // Tauri's default WebView2 args (setting this overrides them) and // appends the Chromium background-throttling disables. Windows-only // in effect; harmless elsewhere. Does not block system sleep. - .additional_browser_args( - "--disable-features=msWebOOUI,msPdfOOUI --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows", - ) + .additional_browser_args(&webview2_browser_args( + std::env::var("LOTUS_WEBVIEW2_DEBUG_PORT").ok().as_deref(), + )) .on_page_load(move |window, payload| { if matches!(payload.event(), PageLoadEvent::Finished) { // Reveal only on the FIRST settle: later page loads (e.g. a @@ -1289,6 +1289,44 @@ pub fn run() { .expect("error while building tauri application"); } +/// WebView2 browser arguments. Setting them replaces Tauri's defaults, so +/// they're kept, plus the Chromium background-throttling disables (P5-42). +/// +/// cinny-desktop #19: `LOTUS_WEBVIEW2_DEBUG_PORT=` in the environment +/// opens WebView2's DevTools port on localhost so the Windows smoke test can +/// drive the installed app. WebView2's own `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` +/// doesn't apply because the app sets its arguments explicitly. Off unless the +/// variable holds a valid port; only whoever launches the app can set it. +pub(crate) fn webview2_browser_args(debug_port: Option<&str>) -> String { + let mut args = String::from( + "--disable-features=msWebOOUI,msPdfOOUI --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows", + ); + if let Some(port) = debug_port + .and_then(|p| p.trim().parse::().ok()) + .filter(|p| *p >= 1024) + { + eprintln!("webview: DevTools port {port} open (LOTUS_WEBVIEW2_DEBUG_PORT)"); + args.push_str(&format!(" --remote-debugging-port={port}")); + } + args +} + +#[cfg(test)] +mod webview2_args_tests { + use super::webview2_browser_args; + + #[test] + fn debug_port_only_when_asked_for_and_valid() { + let base = webview2_browser_args(None); + assert!(base.contains("--disable-renderer-backgrounding")); + assert!(!base.contains("remote-debugging")); + assert!(webview2_browser_args(Some("9222")).ends_with(" --remote-debugging-port=9222")); + for bad in ["", "abc", "80", "70000", "9222 --evil", "-1"] { + assert_eq!(webview2_browser_args(Some(bad)), base, "{bad}"); + } + } +} + #[cfg(test)] mod tray_tests { use super::*; From 2be36d14a9805156f786c3baae668e4bfcf7ead2 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 30 Sep 2026 04:01:05 +0000 Subject: [PATCH 10/12] chore: bump cinny submodule to 747400ea (nightly catch-up) --- cinny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinny b/cinny index d6548c5..747400e 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit d6548c56fdeb0e8cba2a235007b50ac85c54fb8c +Subproject commit 747400ea258fbb9fd1297c5e82d0081251bbfff0 From e2e43aa08c9ae04f6d47f18b594e68fe074b7f6a Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 30 Sep 2026 10:07:39 -0400 Subject: [PATCH 11/12] ci(windows-smoke): fetch the branch to build into a worktree A second actions/checkout in the same job fails on the Windows host runner (Access is denied on the cached action's pack file). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .gitea/workflows/windows-smoke.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/windows-smoke.yml b/.gitea/workflows/windows-smoke.yml index ccd0d5f..084e2bd 100644 --- a/.gitea/workflows/windows-smoke.yml +++ b/.gitea/workflows/windows-smoke.yml @@ -43,12 +43,16 @@ jobs: Invoke-WebRequest -Uri "$env:GITEA_URL/$env:REPO/releases/download/latest/LotusChat-x86_64-setup.exe" -OutFile smoke-installer\setup.exe Get-Item smoke-installer\setup.exe | Select-Object Name, Length + # A second actions/checkout in one job trips over the host runner's + # action cache on Windows ("Access is denied" on its pack files), so + # fetch the branch into a worktree with plain git instead. - name: Check out the branch to build if: ${{ inputs.ref != '' }} - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref }} - path: build-src + shell: powershell + run: | + git fetch --depth=1 origin "${{ inputs.ref }}" + git worktree add --force build-src FETCH_HEAD + git -C build-src log --oneline -1 - name: Build the branch if: ${{ inputs.ref != '' }} From 12ad4bf681b09561b3150bbd25045cf49a97b6ab Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 30 Sep 2026 10:32:45 -0400 Subject: [PATCH 12/12] windows smoke: fake capture devices in the opt-in test mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI VM has no microphone (getUserMedia → NotFoundError). With LOTUS_WEBVIEW2_DEBUG_PORT set the app also passes --use-fake-device-for-media-stream; permission requests still go through the real PermissionRequested handler. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src-tauri/src/lib.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 87944b3..d0b9753 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1294,9 +1294,12 @@ pub fn run() { /// /// cinny-desktop #19: `LOTUS_WEBVIEW2_DEBUG_PORT=` in the environment /// opens WebView2's DevTools port on localhost so the Windows smoke test can -/// drive the installed app. WebView2's own `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` -/// doesn't apply because the app sets its arguments explicitly. Off unless the -/// variable holds a valid port; only whoever launches the app can set it. +/// drive the installed app, and gives it Chromium's fake capture devices (the +/// CI VM has no microphone; permission requests still go through the app's +/// real PermissionRequested handler). WebView2's own +/// `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` doesn't apply because the app sets +/// its arguments explicitly. Off unless the variable holds a valid port; only +/// whoever launches the app can set it. pub(crate) fn webview2_browser_args(debug_port: Option<&str>) -> String { let mut args = String::from( "--disable-features=msWebOOUI,msPdfOOUI --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows", @@ -1306,7 +1309,9 @@ pub(crate) fn webview2_browser_args(debug_port: Option<&str>) -> String { .filter(|p| *p >= 1024) { eprintln!("webview: DevTools port {port} open (LOTUS_WEBVIEW2_DEBUG_PORT)"); - args.push_str(&format!(" --remote-debugging-port={port}")); + args.push_str(&format!( + " --remote-debugging-port={port} --use-fake-device-for-media-stream" + )); } args } @@ -1320,7 +1325,8 @@ mod webview2_args_tests { let base = webview2_browser_args(None); assert!(base.contains("--disable-renderer-backgrounding")); assert!(!base.contains("remote-debugging")); - assert!(webview2_browser_args(Some("9222")).ends_with(" --remote-debugging-port=9222")); + assert!(webview2_browser_args(Some("9222")) + .ends_with(" --remote-debugging-port=9222 --use-fake-device-for-media-stream")); for bad in ["", "abc", "80", "70000", "9222 --evil", "-1"] { assert_eq!(webview2_browser_args(Some(bad)), base, "{bad}"); }