mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-01 20:02:30 +00:00
* core: test that history posted before channel signing reaches later joiners as signature missing, and channel moderator delete for all Confirms the case the owner's new alert describes: a subscriber who joins after the owner turned on "Sign messages" sees the earlier posts as "signature missing" and later ones as signed, while earlier subscribers keep them unmarked. Also covers a non-owner channel moderator deleting a contributor's signed post for everyone. Run: 001-channel-signing-moderators, slice 1 * ios: keep "Chat with admins" off when saving group preferences toGroupPreferences left out the support preference, so the saved profile had none and core filled in its default, on: every channel preference save from iOS silently turned on chats with admins, which have no end-to-end encryption in channels. Android and desktop already pass it. Run: 001-channel-signing-moderators, slice 2 * ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing The channel preference was hidden while signing was recipient-only. Owners now see the switch; turning it on asks for confirmation, because subscribers who join later receive the earlier posts unsigned through history and see them as "signature missing". The "Signature missing" explanation now says the message may predate signing or have been forwarded by a relay without its signature, instead of only implying tampering. Run: 001-channel-signing-moderators, slice 3 * ui: channel owners can make subscribers moderators, and moderators see the subscriber list Core already lets a channel owner make a subscriber a moderator; the role picker offered only subscriber and contributor. It now offers moderator too (not admin, by decision). Moderators see the full member list the owner sees, so they can reach the subscribers they may block (subscribers can't post, so there was no other way to reach them), and member info is headed Moderator or Contributor instead of Subscriber for those roles. Run: 001-channel-signing-moderators, slice 4 * ci: long-press and right-click for app tests, safer ui arguments, desktop Kotlin 4 GB, desktop session limits from claude-scope Message menus open only on long-press (iOS) or right-click (desktop), so Claude sessions couldn't reach Moderate, Info or Delete. Ported from f/directory-search unchanged, with its hardening: the guards refuse unquoted glob, brace and tilde expansion, and desktop ui checks numbers and sends one key per call so a key argument can't run an xdotool command. The desktop app job's Kotlin compiler gets 4 GB, as f/directory-search needed after merging master. Desktop sessions take max-turns and max-budget-usd from claude-scope like iOS (60 and 3 when unset, as before), and the Claude job may run 120 minutes. Run: 001-channel-signing-moderators, slice 5 * ci: Claude test plans for channel message signing and channel moderators This branch's iOS device test plan and desktop prompt replace master's smoke tests: the owner creates a channel on the preset relays, turns on Sign messages through its alert, and two more profiles in the same app join by the copied link; the owner makes one a moderator, who then sees the subscriber list, moderates a contributor's message and blocks the contributor. Signatures are checked as text in message Info. One layout, no test backend. Run: 001-channel-signing-moderators, slice 6 * fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing * update plans * fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing * fixup! ci: Claude test plans for channel message signing and channel moderators * fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing * fixup! ci: Claude test plans for channel message signing and channel moderators * fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing * fixup! ci: Claude test plans for channel message signing and channel moderators --------- Co-authored-by: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com>
467 lines
17 KiB
YAML
467 lines
17 KiB
YAML
name: desktop-linux
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
claude:
|
|
description: Also run the Claude job, which spends the Claude subscription's usage limits
|
|
type: boolean
|
|
default: false
|
|
backend:
|
|
description: Also start the test backend
|
|
type: boolean
|
|
default: false
|
|
release:
|
|
description: Also test the latest released app, to tell a branch's failure from the runner's
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
|
|
concurrency:
|
|
group: desktop-linux-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
GHC_VER: 9.6.3
|
|
CABAL_VER: 3.10.2.0
|
|
JAVA_VER: 17
|
|
CORE_PKG: desktop-core-linux-x86_64.tar.gz
|
|
BACKEND_PKG: desktop-linux-backend.tar.gz
|
|
|
|
jobs:
|
|
plan:
|
|
name: choose jobs, find earlier builds
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
outputs:
|
|
run-claude: ${{ steps.jobs.outputs.claude }}
|
|
run-backend: ${{ steps.jobs.outputs.backend }}
|
|
run-release: ${{ steps.jobs.outputs.release }}
|
|
claude-backend: ${{ steps.scope.outputs.backend }}
|
|
core: ${{ steps.names.outputs.core }}
|
|
core-run: ${{ steps.core.outputs.run-id }}
|
|
backend: ${{ steps.names.outputs.backend }}
|
|
backend-run: ${{ steps.backend.outputs.run-id }}
|
|
steps:
|
|
- name: Choose jobs
|
|
id: jobs
|
|
env:
|
|
CLAUDE: ${{ inputs.claude }}
|
|
BACKEND: ${{ inputs.backend }}
|
|
RELEASE: ${{ inputs.release }}
|
|
run: |
|
|
echo "claude=$CLAUDE" | tee -a "$GITHUB_OUTPUT"
|
|
echo "backend=$BACKEND" | tee -a "$GITHUB_OUTPUT"
|
|
echo "release=$RELEASE" | tee -a "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Claude scope
|
|
id: scope
|
|
if: steps.jobs.outputs.claude == 'true'
|
|
env:
|
|
BACKEND: ${{ steps.jobs.outputs.backend }}
|
|
run: |
|
|
backend=$(sed -n 's/^backend: *//p' scripts/ci/desktop-linux/claude-scope)
|
|
case "$backend" in
|
|
yes | no) echo "backend=$backend" | tee -a "$GITHUB_OUTPUT" ;;
|
|
*)
|
|
echo "::error::claude-scope must say backend: yes or backend: no"
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ "$backend" = yes ] && [ "$BACKEND" != true ]; then
|
|
echo "::error::claude-scope says the Claude session uses the test backend: ask for it too"
|
|
exit 1
|
|
fi
|
|
|
|
# artifacts are named after the sources they are built from, so a run uses one that an earlier run uploaded
|
|
- name: Artifact names
|
|
id: names
|
|
run: |
|
|
echo "core=desktop-linux-core-${{ hashFiles('src/**', 'cabal.project', 'simplex-chat.cabal', 'PRIVACY.md', 'scripts/desktop/build-lib-linux.sh', 'scripts/ci/desktop-linux/build-core.sh') }}" >> "$GITHUB_OUTPUT"
|
|
echo "backend=desktop-linux-backend-${{ hashFiles('src/**', 'apps/simplex-chat/**', 'apps/simplex-directory-service/Main.hs', 'apps/simplex-directory-service/src/**', 'cabal.project', 'simplex-chat.cabal', 'scripts/ci/desktop-linux/build-backend.sh') }}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Find core library
|
|
id: core
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: scripts/ci/desktop-linux/earlier-run.sh "${{ steps.names.outputs.core }}"
|
|
|
|
- name: Find backend
|
|
id: backend
|
|
if: steps.jobs.outputs.backend == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: scripts/ci/desktop-linux/earlier-run.sh "${{ steps.names.outputs.backend }}"
|
|
|
|
backend:
|
|
name: test backend for the app
|
|
needs: plan
|
|
if: needs.plan.outputs.run-backend == 'true' && needs.plan.outputs.backend-run == ''
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 150
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Show runner
|
|
run: |
|
|
lsb_release -ds
|
|
nproc
|
|
free -h
|
|
df -h /
|
|
|
|
- name: Setup Haskell
|
|
uses: simplex-chat/setup-haskell-action@v2
|
|
with:
|
|
ghc-version: ${{ env.GHC_VER }}
|
|
cabal-version: ${{ env.CABAL_VER }}
|
|
|
|
- name: Install libraries
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libffi-dev libgmp-dev libnuma-dev libssl-dev zlib1g-dev
|
|
|
|
- name: Build backend
|
|
run: scripts/ci/desktop-linux/build-backend.sh ~/backend/${{ env.BACKEND_PKG }}
|
|
|
|
- name: Upload backend
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ needs.plan.outputs.backend }}
|
|
path: ~/backend/${{ env.BACKEND_PKG }}
|
|
compression-level: 0
|
|
retention-days: 30
|
|
if-no-files-found: error
|
|
|
|
core:
|
|
name: core library for Linux
|
|
needs: plan
|
|
if: needs.plan.outputs.core-run == ''
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 150
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Show runner
|
|
run: |
|
|
lsb_release -ds
|
|
nproc
|
|
free -h
|
|
df -h /
|
|
|
|
- name: Setup Haskell
|
|
uses: simplex-chat/setup-haskell-action@v2
|
|
with:
|
|
ghc-version: ${{ env.GHC_VER }}
|
|
cabal-version: ${{ env.CABAL_VER }}
|
|
|
|
- name: Install libraries
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libffi-dev libgmp-dev libnuma-dev libssl-dev zlib1g-dev patchelf
|
|
|
|
- name: Build core library
|
|
run: scripts/ci/desktop-linux/build-core.sh ~/core/${{ env.CORE_PKG }}
|
|
|
|
- name: Upload core library
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ needs.plan.outputs.core }}
|
|
path: ~/core/${{ env.CORE_PKG }}
|
|
compression-level: 0
|
|
retention-days: 30
|
|
if-no-files-found: error
|
|
|
|
app:
|
|
name: desktop app under Xvfb
|
|
needs: [plan, core, backend]
|
|
# core and backend are skipped when an earlier run built them, and backend also when the run doesn't start it
|
|
if: ${{ !cancelled() && !failure() && needs.plan.result == 'success' }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Show runner
|
|
run: |
|
|
lsb_release -ds
|
|
nproc
|
|
free -h
|
|
df -h /
|
|
echo "ANDROID_HOME=$ANDROID_HOME"
|
|
ls "$ANDROID_HOME/platforms" "$ANDROID_HOME/build-tools" || true
|
|
|
|
- name: Install tools
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
|
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
|
|
|
- name: Download core library
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ${{ needs.plan.outputs.core }}
|
|
path: ~/core
|
|
run-id: ${{ needs.plan.outputs.core-run || github.run_id }}
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Install core library
|
|
id: core
|
|
run: scripts/ci/desktop-linux/install-core.sh ~/core/${{ env.CORE_PKG }}
|
|
|
|
- name: Download backend
|
|
if: needs.plan.outputs.run-backend == 'true'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ${{ needs.plan.outputs.backend }}
|
|
path: ~/backend
|
|
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Start test backend
|
|
id: backend
|
|
if: needs.plan.outputs.run-backend == 'true'
|
|
timeout-minutes: 30
|
|
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
|
|
|
|
# the app and its tests are still built when the backend fails
|
|
- name: Setup Java
|
|
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: corretto
|
|
java-version: ${{ env.JAVA_VER }}
|
|
|
|
# the Kotlin compiler daemon otherwise gets gradle.properties' 2 GB, which compiling :common runs out of
|
|
- name: Build app
|
|
id: build
|
|
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
|
|
working-directory: apps/multiplatform
|
|
run: |
|
|
./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets -Pkotlin.daemon.jvmargs=-Xmx4g :desktop:createDistributable
|
|
app=release/main/app/simplex
|
|
ls -la "$app/bin" "$app/lib/app/resources"
|
|
du -sh "$app"
|
|
LD_LIBRARY_PATH="$app/lib/app/resources" ldd "$app/lib/app/resources/libapp-lib.so"
|
|
|
|
# a tarball keeps the app's file modes, which an artifact drops
|
|
- name: Pack app
|
|
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
|
run: tar -czf "$RUNNER_TEMP/desktop-app.tar.gz" -C apps/multiplatform/release/main/app simplex
|
|
|
|
- name: Upload app
|
|
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-app
|
|
path: ${{ runner.temp }}/desktop-app.tar.gz
|
|
compression-level: 0
|
|
retention-days: 14
|
|
if-no-files-found: error
|
|
|
|
- name: Run desktop tests
|
|
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
|
working-directory: apps/multiplatform
|
|
run: ./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets -Pkotlin.daemon.jvmargs=-Xmx4g :common:desktopTest
|
|
|
|
- name: Report desktop tests
|
|
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
|
run: scripts/ci/desktop-linux/test-summary.py apps/multiplatform/common/build/test-results/desktopTest
|
|
|
|
- name: Run smoke test
|
|
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.backend.outcome != 'failure' }}
|
|
env:
|
|
BACKEND: ${{ steps.backend.outcome }}
|
|
run: |
|
|
if [ "$BACKEND" = success ]; then
|
|
export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server")
|
|
cases=backend
|
|
fi
|
|
scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test" ${cases:-}
|
|
|
|
- name: Stop test backend
|
|
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
|
|
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
|
|
|
|
- name: Upload smoke test results
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-smoke-test
|
|
path: ${{ runner.temp }}/smoke-test
|
|
retention-days: 3
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload desktop test report
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-test-report
|
|
path: |
|
|
apps/multiplatform/common/build/reports/tests/desktopTest
|
|
apps/multiplatform/common/build/test-results/desktopTest
|
|
retention-days: 3
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload backend logs
|
|
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-backend-logs
|
|
path: ${{ runner.temp }}/backend/*.log
|
|
retention-days: 3
|
|
if-no-files-found: warn
|
|
|
|
# Claude Code repeats the smoke test with the ui helper. Each run spends the Claude subscription's usage
|
|
# limits, so it runs only when asked for. The app-sim-claude environment's branch rule decides which branches
|
|
# may use the token.
|
|
claude:
|
|
name: Claude tests the desktop app
|
|
needs: [plan, app]
|
|
if: ${{ !cancelled() && needs.plan.outputs.run-claude == 'true' && needs.app.result == 'success' }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 120
|
|
environment:
|
|
name: app-sim-claude
|
|
deployment: false
|
|
concurrency:
|
|
group: desktop-linux-claude-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
env:
|
|
# the same in both app workflows
|
|
CLAUDE_CODE_VERSION: 2.1.300
|
|
CLAUDE_MODEL: claude-haiku-5-5
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install tools
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
|
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
|
|
|
- name: Download app
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: desktop-app
|
|
path: ${{ runner.temp }}
|
|
|
|
- name: Unpack app
|
|
run: |
|
|
mkdir -p "$RUNNER_TEMP/app"
|
|
tar -xzf "$RUNNER_TEMP/desktop-app.tar.gz" -C "$RUNNER_TEMP/app"
|
|
|
|
- name: Download backend
|
|
if: needs.plan.outputs.claude-backend == 'yes'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ${{ needs.plan.outputs.backend }}
|
|
path: ~/backend
|
|
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Start test backend
|
|
id: backend
|
|
if: needs.plan.outputs.claude-backend == 'yes'
|
|
timeout-minutes: 30
|
|
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
|
|
|
|
# ui goes on every shell's PATH, where Claude's Bash tool finds it whatever environment it builds
|
|
- name: Install Claude Code and the ui command
|
|
run: |
|
|
npm install --prefix "$RUNNER_TEMP/claude-code" "@anthropic-ai/claude-code@$CLAUDE_CODE_VERSION"
|
|
echo "$RUNNER_TEMP/claude-code/node_modules/.bin" >> "$GITHUB_PATH"
|
|
printf '#!/bin/sh\nexec %s/scripts/ci/desktop-linux/ui "$@"\n' "$PWD" | sudo tee /usr/local/bin/ui
|
|
sudo chmod +x /usr/local/bin/ui
|
|
|
|
- name: Start the app
|
|
run: scripts/ci/desktop-linux/smoke-test.sh "$RUNNER_TEMP/app/simplex" "$RUNNER_TEMP/claude-test/screens" start
|
|
|
|
# Claude's Bash is limited to single ui commands by the guard hook in claude-test.sh. Claude Code's credential
|
|
# scrub stays off: on Linux it forces its whole Bash sandbox (bubblewrap, socat), which ui isn't set up for.
|
|
- name: Run Claude
|
|
id: claude
|
|
env:
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
|
|
run: scripts/ci/desktop-linux/claude-test.sh "$RUNNER_TEMP/claude-test" "$RUNNER_TEMP/claude-transcript.jsonl"
|
|
|
|
- name: Report Claude usage
|
|
if: ${{ !cancelled() && steps.claude.outcome != 'skipped' }}
|
|
run: scripts/ci/desktop-linux/claude-usage.py "$RUNNER_TEMP/claude-transcript.jsonl" "$RUNNER_TEMP/claude-usage.json"
|
|
|
|
- name: Stop test backend
|
|
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
|
|
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
|
|
|
|
- name: Upload Claude test results
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-claude-test
|
|
path: |
|
|
${{ runner.temp }}/claude-test
|
|
${{ runner.temp }}/claude-transcript.jsonl
|
|
${{ runner.temp }}/claude-transcript.jsonl.stderr
|
|
${{ runner.temp }}/claude-transcript.jsonl.guard.log
|
|
${{ runner.temp }}/claude-usage.json
|
|
retention-days: 7
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload backend logs
|
|
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-claude-backend-logs
|
|
path: ${{ runner.temp }}/backend/*.log
|
|
retention-days: 3
|
|
if-no-files-found: warn
|
|
|
|
# the latest release's onboarding, to tell a branch's own failure from the runner's
|
|
release:
|
|
name: released app under Xvfb
|
|
needs: plan
|
|
if: needs.plan.outputs.run-release == 'true'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
filter: tree:0
|
|
|
|
- name: Download released app
|
|
run: scripts/ci/desktop-linux/release-app.sh "$RUNNER_TEMP/release"
|
|
|
|
- name: Install UI tools
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr
|
|
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr
|
|
|
|
- name: Run smoke test
|
|
run: scripts/ci/desktop-linux/smoke-test.sh "$RUNNER_TEMP/release/app/opt/simplex" "$RUNNER_TEMP/smoke-test" onboarding
|
|
|
|
- name: Upload smoke test results
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: desktop-release-smoke-test
|
|
path: ${{ runner.temp }}/smoke-test
|
|
retention-days: 3
|
|
if-no-files-found: warn
|