mirror of
https://github.com/wekan/wekan.git
synced 2026-11-04 17:10:08 -05:00
cpu-exec qemu-user fallback everywhere; tests for all newest fixes.
Fix #6458 completely: the reporter's host runs inside a hypervisor that masks AVX, and the old per-tool AVX wrappers (amd64-only, PATH-based) were bypassed by absolute-path mongod invocations. - New general snap-src/bin/cpu-exec: runs any binary directly when the CPU has the features it declares (--features x86_64=avx,aarch64=atomics or WEKAN_REQUIRED_CPU_FEATURES) and through same-arch qemu-user when not; plain zero-overhead exec with no declared features. Lookup order: WEKAN_QEMU_USER, qemu-<arch> next to the script (bundle/Docker), $SNAP/qemu-<arch>, the snap's migratemongo/avx one, then system qemu-user(-static). Clear error + direct run when no qemu exists, so the real SIGILL surfaces instead of hiding. - Snap: mongodb-control and migration-control run every mongod 7 through cpu-exec, so MongoDB works without AVX (slower) and migration can READ modern MongoDB data on such CPUs; completed-FerretDB switch and migration fallbacks unchanged (switch_to_completed_ferretdb extracted). - release-all.yml: every Linux bundle .zip ships cpu-exec + this-arch's static qemu-user (amd64/arm64/ppc64le/s390x/riscv64; stripped from win64/mac bundles), flowing into Docker and Snap automatically; Sandstorm .spk gets both via build-deps.sh. Bundle launcher and Docker entrypoint route node/ferretdb through cpu-exec. - Docs: mongodb-avx-qemu.md and mongodb-raspi4-qemu.md document the built-in mechanism. Tests, with negative cases, for the newest features and fixes at changelog: htmljs array-vs-attributes (#6459), description draft normalize/reset (#6455), comment draft lifecycle (#5547), attachment delete guard (#5282), rules move actions + import normalization (#6472), snap mongod-failure/migration recovery (#6458 #6466), FerretDB polling defaults (#6467 #6468), UI density guards for every #6465 fix, and a behavioral cpu-exec test against fake cpuinfo + fake qemu. All wired into test:unit:node. Go table tests for the FerretDB SQLite filter pushdown are in the wekan/FerretDB repo. Updated ChangeLog. Thanks to xet7.
This commit is contained in:
1 parent
ed19445d30
commit
794346e396
21 files changed
+1271
-46
No files matched your search
@@ -309,7 +309,7 @@ jobs:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
|
||||
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip qemu-user-static
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -361,6 +361,16 @@ jobs:
|
||||
# Self-contained launcher (defaults to FerretDB SQLite + WRITABLE_PATH).
|
||||
cp releases/ferretdb/start-wekan.sh .build/bundle/
|
||||
chmod +x .build/bundle/start-wekan.sh
|
||||
# #6458: cpu-exec + same-arch qemu-user. cpu-exec runs any bundled
|
||||
# binary that declares required CPU features (e.g. an external
|
||||
# MongoDB 5+: AVX on x86_64) through qemu-user emulation on CPUs
|
||||
# that lack them — including hypervisors that mask AVX. Every Linux
|
||||
# bundle ships its own arch's qemu-user; other arches overwrite
|
||||
# these below.
|
||||
cp snap-src/bin/cpu-exec .build/bundle/cpu-exec
|
||||
chmod +x .build/bundle/cpu-exec
|
||||
cp /usr/bin/qemu-x86_64-static .build/bundle/qemu-x86_64
|
||||
chmod +x .build/bundle/qemu-x86_64
|
||||
|
||||
- name: Create amd64 zip
|
||||
run: |
|
||||
@@ -384,7 +394,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip qemu-user-static
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -430,6 +440,13 @@ jobs:
|
||||
curl -fSL --retry 5 --retry-delay 10 -o "bundle/$t" "$MT/$t-arm64"
|
||||
chmod +x "bundle/$t"
|
||||
done
|
||||
# #6458: replace the inherited amd64 qemu-user with this arch's own
|
||||
# (cpu-exec is a shell script and stays as inherited). qemu-aarch64
|
||||
# lets ARMv8.0 hosts (Raspberry Pi 4 and older) run binaries that
|
||||
# need ARMv8.2-A, like MongoDB 5+.
|
||||
rm -f bundle/qemu-x86_64
|
||||
cp /usr/bin/qemu-aarch64-static bundle/qemu-aarch64
|
||||
chmod +x bundle/qemu-aarch64
|
||||
|
||||
- name: Create arm64 zip
|
||||
run: |
|
||||
@@ -518,6 +535,8 @@ jobs:
|
||||
rm -f bundle/node bundle/ferretdb bundle/.ferretdb-default bundle/start-wekan.sh
|
||||
rm -f bundle/bsondump bundle/mongodump bundle/mongoexport bundle/mongofiles \
|
||||
bundle/mongoimport bundle/mongorestore bundle/mongostat bundle/mongotop
|
||||
# Linux-only cpu-exec + qemu-user (#6458) make no sense on Windows.
|
||||
rm -f bundle/cpu-exec bundle/qemu-x86_64
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
@@ -625,6 +644,9 @@ jobs:
|
||||
# inherited from the amd64 base bundle with the macOS builds (added in a
|
||||
# later step). The POSIX launcher start-wekan.sh works on macOS, so keep it.
|
||||
rm -f bundle/node bundle/ferretdb bundle/.ferretdb-default
|
||||
# Linux-only cpu-exec + qemu-user (#6458): qemu-user does not exist for
|
||||
# macOS (Apple Silicon needs no AVX emulation anyway).
|
||||
rm -f bundle/cpu-exec bundle/qemu-x86_64
|
||||
rm -f bundle/bsondump bundle/mongodump bundle/mongoexport bundle/mongofiles \
|
||||
bundle/mongoimport bundle/mongorestore bundle/mongostat bundle/mongotop
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
@@ -785,6 +807,21 @@ jobs:
|
||||
# launcher. /bundle is the host bundle/ dir (see -v mount above).
|
||||
cp /opt/node/bin/node /bundle/node
|
||||
chmod +x /bundle/node
|
||||
# #6458: replace the inherited amd64 qemu-user with THIS arch own
|
||||
# same-arch qemu-user (from the target-arch distro, so it runs on
|
||||
# this arch), used by bundle/cpu-exec to emulate CPU features a
|
||||
# binary needs but the host CPU lacks. Tolerant: if the package
|
||||
# is unavailable on this arch, the bundle just ships without it
|
||||
# (cpu-exec then falls back to a system qemu-user, or reports).
|
||||
rm -f /bundle/qemu-x86_64
|
||||
if apt-get install -y -q qemu-user-static >/dev/null 2>&1 && \
|
||||
[ -x "/usr/bin/qemu-$(uname -m)-static" ]; then
|
||||
cp "/usr/bin/qemu-$(uname -m)-static" "/bundle/qemu-$(uname -m)"
|
||||
chmod +x "/bundle/qemu-$(uname -m)"
|
||||
echo "Bundled qemu-$(uname -m) for cpu-exec."
|
||||
else
|
||||
echo "qemu-user-static not available for $(uname -m); bundle ships without a bundled qemu-user."
|
||||
fi
|
||||
'
|
||||
|
||||
- name: Embed FerretDB (${{ matrix.arch }}) into the bundle
|
||||
@@ -864,7 +901,7 @@ jobs:
|
||||
- name: Install system dependencies and Meteor
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git qemu-user-static
|
||||
if [ ! -f "$HOME/.meteor/meteor" ]; then curl https://install.meteor.com/ | sh; fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Install system dependencies and Meteor
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git qemu-user-static
|
||||
if [ ! -f "$HOME/.meteor/meteor" ]; then curl https://install.meteor.com/ | sh; fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
|
||||
@@ -102,6 +102,45 @@ This release adds the following updates:
|
||||
- [Bump websocket-driver from 0.7.4 to 0.7.5](https://github.com/wekan/wekan/pull/6462).
|
||||
Thanks to dependabot.
|
||||
|
||||
- **New general `cpu-exec` + bundled qemu-user: every WeKan platform now runs binaries that need
|
||||
missing CPU features through emulation automatically**
|
||||
([#6458](https://github.com/wekan/wekan/issues/6458), `snap-src/bin/cpu-exec`,
|
||||
`snap-src/bin/mongodb-control`, `snap-src/bin/migration-control`, `.github/workflows/release-all.yml`,
|
||||
`.github/workflows/sandstorm.yml`, `sandstorm-src/build-deps.sh`, `releases/ferretdb/start-wekan.sh`,
|
||||
`releases/ferretdb/wekan-entrypoint.sh`, `docs/Databases/mongodb-avx-qemu.md`,
|
||||
`docs/Databases/mongodb-raspi4-qemu.md`). The #6458 report turned out to run inside a hypervisor
|
||||
that MASKS AVX — and the snap's old per-tool AVX wrappers (amd64-only, PATH-based) were bypassed
|
||||
by every absolute-path `mongod` invocation. The new `cpu-exec` helper is one general mechanism
|
||||
for all scripts, sandboxes, platforms and CPUs: `cpu-exec --features x86_64=avx,aarch64=atomics
|
||||
<binary> [args]` checks `/proc/cpuinfo` and, when a required feature is missing, transparently
|
||||
re-runs the binary through a same-architecture qemu-user (bundled first, then system); with no
|
||||
declared features it is a plain zero-overhead exec, so every binary can be routed through it
|
||||
(`WEKAN_REQUIRED_CPU_FEATURES` declares requirements externally). The Snap's mongodb-control and
|
||||
migration-control now run every mongod 7 through it — so MongoDB works (slower) on CPUs without
|
||||
AVX and the migration can READ modern MongoDB data there, with the FerretDB switch/migration
|
||||
fallbacks unchanged; `aarch64=atomics` covers MongoDB's ARMv8.2-A requirement (Raspberry Pi 4
|
||||
and older lack it). release-all.yml now ships `cpu-exec` plus this-arch's static qemu-user in
|
||||
every Linux bundle .zip (amd64/arm64/ppc64le/s390x/riscv64 — stripped from the Windows/macOS
|
||||
bundles, where qemu-user does not exist), which flows into the Docker image and the Snap
|
||||
automatically, and the Sandstorm .spk gets both via build-deps.sh; the bundle launcher and
|
||||
Docker entrypoint route node/ferretdb through it. Thanks to a1bert01 and xet7.
|
||||
|
||||
- **Added regression tests, with negative cases, for all of the fixes below** (`tests/htmljsArrayContent.test.cjs`,
|
||||
`tests/cardDescriptionDraft.test.cjs`, `tests/commentDraft.test.cjs`, `tests/attachmentDeleteGuard.test.cjs`,
|
||||
`tests/ruleMoveAction.test.cjs`, `tests/snapMigrationRecovery.test.cjs`, `tests/ferretdbPolling.test.cjs`,
|
||||
`tests/uiDensity.test.cjs`, `tests/cpuExec.test.cjs` — a BEHAVIORAL test that executes the real
|
||||
cpu-exec against fake /proc/cpuinfo files and a fake qemu-user, covering direct exec, qemu fallback,
|
||||
missing-qemu error, per-arch scoping and env overrides — wired into `test:unit:node` in
|
||||
`package.json`; plus Go table tests in the wekan/FerretDB fork's
|
||||
`internal/backends/sqlite/query_test.go`). The htmljs test exercises the vendored
|
||||
compiler's Tag constructor directly (array content vs. attributes, #6459) and the draft tests run the real
|
||||
extracted `normalize`/`normalizeTrigger` functions; the UI tests guard the #6465 density fixes in the repo's
|
||||
CSS-guard style (base font 14px, card details docking right of the board instead of over its own card, compact
|
||||
admin table headers, un-clipped zoom pill, one-click board settings cog). The FerretDB Go tests pin the SQLite
|
||||
filter-pushdown semantics: which strings are pushdown-safe, exact WHERE/args for `_id` and top-level equality
|
||||
filters, and that dotted paths, operators, non-strings and unsafe strings stay with the in-Go filter. Thanks
|
||||
to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- **GUI: the "More" menu of cards was completely empty, so cards could not be deleted from it**
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
## Built-in: WeKan cpu-exec runs binaries with qemu-user automatically
|
||||
|
||||
Since WeKan 9.96 (issue [#6458](https://github.com/wekan/wekan/issues/6458)), WeKan ships a general
|
||||
`cpu-exec` helper on every Linux platform — in the Snap (`$SNAP/bin/cpu-exec`), in every
|
||||
`wekan-<version>-<arch>.zip` bundle (`bundle/cpu-exec`, next to a same-arch `qemu-<arch>` static
|
||||
binary), in the Docker image (`/build/cpu-exec`) and in the Sandstorm .spk. It checks
|
||||
`/proc/cpuinfo` for the CPU features a binary needs and, when one is missing, transparently re-runs
|
||||
the binary through qemu-user (which implements the full modern instruction set):
|
||||
|
||||
```
|
||||
cpu-exec --features x86_64=avx,aarch64=atomics mongod --dbpath ...
|
||||
```
|
||||
|
||||
- With no `--features` (or no entry for the current architecture) it is a plain `exec` — zero
|
||||
overhead, safe for every binary.
|
||||
- `WEKAN_REQUIRED_CPU_FEATURES` (same syntax) overrides `--features`, so any script or user can
|
||||
declare requirements without editing commands.
|
||||
- qemu-user lookup order: `$WEKAN_QEMU_USER`, `qemu-<arch>` next to cpu-exec (the bundle),
|
||||
`$SNAP/qemu-<arch>`, the Snap's `migratemongo/avx/qemu-x86_64`, then `qemu-<arch>`/
|
||||
`qemu-<arch>-static` on `$PATH` (install with `sudo apt install qemu-user-static`).
|
||||
- `aarch64=atomics` (LSE, ARMv8.1) is the practical `/proc/cpuinfo` proxy for MongoDB's ARMv8.2-A
|
||||
requirement: Raspberry Pi 3/4 (Cortex-A53/A72) lack it, ARMv8.2-A cores have it.
|
||||
|
||||
The Snap's `mongodb-control` and `migration-control` run every `mongod 7` invocation through
|
||||
cpu-exec, so WeKan's MongoDB works (slower) on CPUs without AVX — including hypervisors/sandboxes
|
||||
that mask AVX — and the MongoDB → FerretDB migration can read modern MongoDB data there too.
|
||||
FerretDB (pure Go + SQLite) needs no special CPU features and is the native-speed alternative:
|
||||
`snap run wekan.migrate`.
|
||||
|
||||
## Meteor podcast about using MongoDB at unsupported CPUs
|
||||
|
||||
- https://www.youtube.com/watch?v=bnU9bUVeN04
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
## Built-in: WeKan cpu-exec does this automatically
|
||||
|
||||
Since WeKan 9.96, the manual wrapper below is built in: every Linux WeKan bundle/Snap/Docker
|
||||
image ships a general `cpu-exec` helper (plus a same-arch `qemu-<arch>` static binary in the
|
||||
bundles) that runs any binary through qemu-user automatically when the CPU lacks features the
|
||||
binary needs — e.g. MongoDB 5+ on Raspberry Pi 4 and older (ARMv8.0, no ARMv8.2-A):
|
||||
|
||||
```
|
||||
cpu-exec --features x86_64=avx,aarch64=atomics mongod --config /etc/mongod.conf
|
||||
```
|
||||
|
||||
See [mongodb-avx-qemu.md](mongodb-avx-qemu.md) for details. The manual steps below still work
|
||||
for a system-wide MongoDB outside WeKan.
|
||||
|
||||
## Meteor podcast about using MongoDB at unsupported CPUs
|
||||
|
||||
- https://www.youtube.com/watch?v=bnU9bUVeN04
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "meteor test --once --driver-package meteortesting:mocha",
|
||||
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/ruleActionResolve.test.cjs && node tests/boardBackground.test.cjs && node tests/weekStart.test.cjs && node tests/ldapUsernameSlug.test.cjs && node tests/ldapGroupAllowlist.test.cjs && node tests/editorSubmitKey.test.cjs && node tests/isoWeek.test.cjs && node tests/boardCopyDefaults.test.cjs && node tests/linkedCardTitle.test.cjs && node tests/commentActivity.test.cjs && node tests/swimlaneSort.test.cjs && node tests/subtaskCopy.test.cjs && node tests/scrollbarCss.test.cjs && node tests/minicardBidiIsolation.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/popupOffset.test.cjs && node tests/linkedCardCover.test.cjs && node tests/inviteEmailLanguage.test.cjs && node tests/domainTablePage.test.cjs && node tests/mongoSelectorSafety.test.cjs && node tests/cardsLoading.test.cjs && node tests/base64Chunk.test.cjs && node tests/backupPaths.test.cjs && node tests/backupCron.test.cjs && node tests/avatarUrlSafety.test.cjs && node tests/importedUserReconciliationPlan.test.cjs && node tests/ldapPhoto.test.cjs && node tests/importedBoardMemberPlan.test.cjs && node tests/unit/resetPasswordEmail.test.js",
|
||||
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/ruleActionResolve.test.cjs && node tests/boardBackground.test.cjs && node tests/weekStart.test.cjs && node tests/ldapUsernameSlug.test.cjs && node tests/ldapGroupAllowlist.test.cjs && node tests/htmljsArrayContent.test.cjs && node tests/cardDescriptionDraft.test.cjs && node tests/commentDraft.test.cjs && node tests/attachmentDeleteGuard.test.cjs && node tests/ruleMoveAction.test.cjs && node tests/snapMigrationRecovery.test.cjs && node tests/cpuExec.test.cjs && node tests/ferretdbPolling.test.cjs && node tests/uiDensity.test.cjs && node tests/editorSubmitKey.test.cjs && node tests/isoWeek.test.cjs && node tests/boardCopyDefaults.test.cjs && node tests/linkedCardTitle.test.cjs && node tests/commentActivity.test.cjs && node tests/swimlaneSort.test.cjs && node tests/subtaskCopy.test.cjs && node tests/scrollbarCss.test.cjs && node tests/minicardBidiIsolation.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/popupOffset.test.cjs && node tests/linkedCardCover.test.cjs && node tests/inviteEmailLanguage.test.cjs && node tests/domainTablePage.test.cjs && node tests/mongoSelectorSafety.test.cjs && node tests/cardsLoading.test.cjs && node tests/base64Chunk.test.cjs && node tests/backupPaths.test.cjs && node tests/backupCron.test.cjs && node tests/avatarUrlSafety.test.cjs && node tests/importedUserReconciliationPlan.test.cjs && node tests/ldapPhoto.test.cjs && node tests/importedBoardMemberPlan.test.cjs && node tests/unit/resetPasswordEmail.test.js",
|
||||
"test:unit:all": "npm run test:unit:node && node tests/cards.stickers.test.js && node tests/stickers.test.js && node tests/stickers.packs.test.js && node tests/linkCardPopup.test.js && node tests/trelloCreator.import.test.js && node tests/trelloImport.maplink.test.js && node tests/trelloImport.zip.test.js && node tests/wekanCreator.import.test.js && node tests/wekanCreator.inconsistent.test.js && node tests/unit/oauth2-admin-groups.test.js",
|
||||
"test:watch": "meteor test --driver-package meteortesting:mocha",
|
||||
"test:e2e": "node tests/e2e/list-regressions.js",
|
||||
|
||||
@@ -78,11 +78,19 @@ if [ "$want_ferret" = true ]; then
|
||||
export METEOR_POLLING_INTERVAL_MS="${METEOR_POLLING_INTERVAL_MS:-30000}"
|
||||
fi
|
||||
|
||||
# #6458: $DIR/cpu-exec runs a binary through the bundled same-arch qemu-user
|
||||
# when the CPU lacks features the binary declares (WEKAN_REQUIRED_CPU_FEATURES,
|
||||
# e.g. "x86_64=avx" for an external MongoDB 5+). node and ferretdb are baseline
|
||||
# builds needing no special features, so with none declared this is a plain
|
||||
# exec — but every binary launch here is feature-safe on every platform.
|
||||
CPU_EXEC="$DIR/cpu-exec"
|
||||
[ -x "$CPU_EXEC" ] || CPU_EXEC=""
|
||||
|
||||
while true; do
|
||||
if [ "$want_ferret" = true ]; then
|
||||
export DO_NOT_TRACK=1 FERRETDB_TELEMETRY=disable
|
||||
echo "Starting bundled FerretDB v1 (SQLite) on $FERRETDB_LISTEN_ADDR (data: $FERRETDB_SQLITE_DIR) ..."
|
||||
"$FERRETDB_BIN" \
|
||||
${CPU_EXEC:+"$CPU_EXEC"} "$FERRETDB_BIN" \
|
||||
--handler=sqlite \
|
||||
--sqlite-url="file:$FERRETDB_SQLITE_DIR/" \
|
||||
--listen-addr="$FERRETDB_LISTEN_ADDR" \
|
||||
@@ -94,7 +102,7 @@ while true; do
|
||||
fi
|
||||
|
||||
echo "Starting WeKan on $ROOT_URL (port $PORT), files under $WRITABLE_PATH ..."
|
||||
"$NODE" "$DIR/main.js" || true
|
||||
${CPU_EXEC:+"$CPU_EXEC"} "$NODE" "$DIR/main.js" || true
|
||||
|
||||
stop_ferret
|
||||
FERRET_PID=""
|
||||
|
||||
@@ -51,15 +51,32 @@ if [ "$want_ferret" = true ]; then
|
||||
# let it be re-enabled). DO_NOT_TRACK/FERRETDB_TELEMETRY are belt-and-suspenders.
|
||||
export DO_NOT_TRACK=1 FERRETDB_TELEMETRY=disable
|
||||
echo "Starting bundled FerretDB v1 (SQLite) on $FERRETDB_LISTEN_ADDR ..."
|
||||
"$FERRETDB_BIN" \
|
||||
--handler=sqlite \
|
||||
--sqlite-url="file:$FERRETDB_SQLITE_DIR/" \
|
||||
--listen-addr="$FERRETDB_LISTEN_ADDR" \
|
||||
--telemetry=disable \
|
||||
--log-level=error &
|
||||
# #6458: /build/cpu-exec runs a binary through the bundled same-arch
|
||||
# qemu-user when the CPU lacks features the binary declares (via
|
||||
# WEKAN_REQUIRED_CPU_FEATURES, e.g. "x86_64=avx"). node and ferretdb are
|
||||
# baseline builds needing no special features, so with none declared this is
|
||||
# a plain exec — but every binary launch here is feature-safe.
|
||||
if [ -x /build/cpu-exec ]; then
|
||||
/build/cpu-exec "$FERRETDB_BIN" \
|
||||
--handler=sqlite \
|
||||
--sqlite-url="file:$FERRETDB_SQLITE_DIR/" \
|
||||
--listen-addr="$FERRETDB_LISTEN_ADDR" \
|
||||
--telemetry=disable \
|
||||
--log-level=error &
|
||||
else
|
||||
"$FERRETDB_BIN" \
|
||||
--handler=sqlite \
|
||||
--sqlite-url="file:$FERRETDB_SQLITE_DIR/" \
|
||||
--listen-addr="$FERRETDB_LISTEN_ADDR" \
|
||||
--telemetry=disable \
|
||||
--log-level=error &
|
||||
fi
|
||||
FERRET_PID=$!
|
||||
trap 'kill "$FERRET_PID" 2>/dev/null || true' EXIT INT TERM
|
||||
fi
|
||||
|
||||
ulimit -s 65500
|
||||
if [ -x /build/cpu-exec ]; then
|
||||
exec /build/cpu-exec node /build/main.js
|
||||
fi
|
||||
exec node /build/main.js
|
||||
@@ -113,6 +113,19 @@ chmod +x "$DEPS/migratemongo/bin/"*
|
||||
echo "==> [6/7] Launcher + importer"
|
||||
cp -f "$REPO/sandstorm-src/start.js" "$DEPS/start.js"
|
||||
cp -f "$REPO/snap-src/bin/migrate-mongo3-to-ferretdb.mjs" "$DEPS/migrate-mongo3-to-ferretdb.mjs"
|
||||
# #6458: cpu-exec + qemu-user — run any binary that needs CPU features the
|
||||
# grain host lacks (e.g. AVX for MongoDB 5+) through qemu-user emulation.
|
||||
# The bundled mongod 3.0/niscud need no special features, so this is
|
||||
# future-proofing; tolerant when qemu-user-static is not installed.
|
||||
cp -f "$REPO/snap-src/bin/cpu-exec" "$DEPS/bin/cpu-exec"
|
||||
chmod +x "$DEPS/bin/cpu-exec"
|
||||
if [ -x /usr/bin/qemu-x86_64-static ]; then
|
||||
cp -f /usr/bin/qemu-x86_64-static "$DEPS/bin/qemu-x86_64"
|
||||
chmod +x "$DEPS/bin/qemu-x86_64"
|
||||
echo " bundled qemu-x86_64 for cpu-exec"
|
||||
else
|
||||
echo " qemu-user-static not installed; spk ships cpu-exec without a bundled qemu-user"
|
||||
fi
|
||||
|
||||
echo "==> [7/7] niscud + old node_modules kept for the niscu->3.0 stage:"
|
||||
ls -la "$DEPS/bin/niscud" "$DEPS/node_modules/mongodb" >/dev/null 2>&1 \
|
||||
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# cpu-exec: run a binary directly when the CPU has every instruction-set
|
||||
# feature the binary needs, otherwise transparently re-run it through
|
||||
# qemu-user emulation (which implements the full modern instruction set).
|
||||
# General mechanism for ALL scripts, sandboxes, platforms and CPUs (#6458).
|
||||
#
|
||||
# Why: MongoDB >= 5.0 x86_64 binaries require AVX — on CPUs without it (old
|
||||
# Intel/AMD, and hypervisors/sandboxes that mask AVX: some QEMU/KVM/Proxmox
|
||||
# configurations, which is exactly the reported #6458 environment) mongod dies
|
||||
# instantly with SIGILL (exit 132). MongoDB >= 5 arm64 similarly requires
|
||||
# ARMv8.2-A (Raspberry Pi 4 and older are ARMv8.0). The snap's old
|
||||
# migratemongo/avx wrappers did this per-tool, amd64-only, and only for PATH
|
||||
# lookups — every script invoking $SNAP/bin/mongod by ABSOLUTE path bypassed
|
||||
# them. This helper replaces that with one general, explicit mechanism.
|
||||
#
|
||||
# Usage:
|
||||
# cpu-exec [--features <arch>=<f1>[+<f2>...][,<arch>=...]] <binary> [args...]
|
||||
#
|
||||
# --features declares the CPU features the binary needs, per architecture:
|
||||
# cpu-exec --features x86_64=avx,aarch64=atomics mongod --dbpath ...
|
||||
# Only the entry matching `uname -m` is checked (against /proc/cpuinfo
|
||||
# flags/Features). No --features, an empty spec, or no entry for the current
|
||||
# architecture means "no special requirements" — the binary is exec'd
|
||||
# directly with zero overhead, so it is safe to route EVERY binary through
|
||||
# cpu-exec. The WEKAN_REQUIRED_CPU_FEATURES environment variable, when set,
|
||||
# overrides --features (same syntax).
|
||||
#
|
||||
# Note: `aarch64=atomics` (LSE, ARMv8.1) is the practical /proc/cpuinfo
|
||||
# proxy for MongoDB's ARMv8.2-A requirement — Cortex-A53/A72 (Raspberry Pi
|
||||
# 3/4) lack it, ARMv8.2-A cores have it.
|
||||
#
|
||||
# qemu-user lookup order (first executable wins):
|
||||
# 1. $WEKAN_QEMU_USER (explicit override)
|
||||
# 2. qemu-<arch> next to this script (bundle .zip / Docker /build)
|
||||
# 3. $SNAP/qemu-<arch>, $SNAP/bin/qemu-<arch>
|
||||
# 4. $SNAP/migratemongo/avx/qemu-x86_64 (existing snap amd64 bundle)
|
||||
# 5. qemu-<arch> / qemu-<arch>-static on PATH
|
||||
# If none is found, a clear error is printed and the binary runs directly so
|
||||
# the real failure (SIGILL) surfaces in the logs instead of hiding.
|
||||
#
|
||||
# WEKAN_CPUINFO overrides the /proc/cpuinfo path (used by the unit tests).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -u
|
||||
|
||||
FEATURES_SPEC=""
|
||||
if [ "${1:-}" = "--features" ]; then
|
||||
FEATURES_SPEC="${2:-}"
|
||||
shift 2
|
||||
fi
|
||||
[ -n "${WEKAN_REQUIRED_CPU_FEATURES:-}" ] && FEATURES_SPEC="${WEKAN_REQUIRED_CPU_FEATURES}"
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: cpu-exec [--features arch=feat1+feat2,arch2=...] <binary> [args...]" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
CPUINFO="${WEKAN_CPUINFO:-/proc/cpuinfo}"
|
||||
|
||||
# Pick this architecture's '+'-separated feature list out of the spec.
|
||||
required=""
|
||||
IFS=',' read -r -a _entries <<< "$FEATURES_SPEC"
|
||||
for _e in "${_entries[@]:-}"; do
|
||||
case "$_e" in
|
||||
"$ARCH"=*) required="${_e#*=}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Which of the required features is the CPU missing? x86 lists them on
|
||||
# "flags :" lines, ARM on "Features :" lines.
|
||||
missing=""
|
||||
if [ -n "$required" ] && [ -r "$CPUINFO" ]; then
|
||||
IFS='+' read -r -a _feats <<< "$required"
|
||||
for _f in "${_feats[@]:-}"; do
|
||||
[ -z "$_f" ] && continue
|
||||
grep -Eiq "^(flags|features)[[:space:]]*:.*[[:space:]]${_f}([[:space:]]|$)" "$CPUINFO" \
|
||||
|| missing="${missing} ${_f}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$missing" ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# The CPU lacks something this binary needs: find a same-architecture
|
||||
# qemu-user to emulate the missing instructions.
|
||||
HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
||||
QEMU=""
|
||||
for candidate in \
|
||||
"${WEKAN_QEMU_USER:-}" \
|
||||
"${HERE:+$HERE/qemu-$ARCH}" \
|
||||
"${SNAP:+$SNAP/qemu-$ARCH}" \
|
||||
"${SNAP:+$SNAP/bin/qemu-$ARCH}" \
|
||||
"${SNAP:+$SNAP/migratemongo/avx/qemu-$ARCH}" \
|
||||
"$(command -v "qemu-$ARCH" 2>/dev/null)" \
|
||||
"$(command -v "qemu-$ARCH-static" 2>/dev/null)"; do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||
QEMU="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$QEMU" ]; then
|
||||
echo "cpu-exec: this CPU lacks${missing}; running $1 through qemu-user emulation ($QEMU)." >&2
|
||||
echo "cpu-exec: emulation is slower — consider migrating to FerretDB (no special CPU features needed)." >&2
|
||||
exec "$QEMU" "$@"
|
||||
fi
|
||||
|
||||
echo "cpu-exec: ERROR: this CPU lacks${missing} and no qemu-user (qemu-$ARCH) was found;" >&2
|
||||
echo "cpu-exec: running $1 directly — expect SIGILL (Illegal instruction). Install qemu-user(-static) or migrate to FerretDB." >&2
|
||||
exec "$@"
|
||||
@@ -90,13 +90,15 @@ else
|
||||
fi
|
||||
log "Temporary source mongod WiredTiger cache: ${SRC_WT_CACHE_GB} GB (RAM-aware; leaves room for FerretDB + importer)"
|
||||
|
||||
start_mongod() { # $1 = mongod binary, $2 = LD_LIBRARY_PATH prefix (may be empty), $3 = extra args (optional)
|
||||
start_mongod() { # $1 = mongod binary, $2 = LD_LIBRARY_PATH prefix (may be empty), $3 = extra args (optional), $4 = CPU feature spec for cpu-exec (optional)
|
||||
rm -f "$SRC_PIDFILE"
|
||||
# --pidfilepath records the forked daemon's PID so force_stop_src can kill
|
||||
# exactly this temporary mongod before we hand back to the normal mongod start
|
||||
# — otherwise a lingering probe mongod would still hold the WiredTiger dbpath
|
||||
# lock and the real start would fail with "Unable to lock the lock file".
|
||||
LD_LIBRARY_PATH="${2:+$2:}$LD_LIBRARY_PATH" "$1" \
|
||||
# #6458: cpu-exec runs the binary through qemu-user when the CPU lacks the
|
||||
# features in $4 (e.g. AVX for mongod 7); with an empty $4 it is a plain exec.
|
||||
LD_LIBRARY_PATH="${2:+$2:}$LD_LIBRARY_PATH" bash "$SNAP/bin/cpu-exec" --features "${4:-}" "$1" \
|
||||
--dbpath "$DBPATH" --bind_ip 127.0.0.1 --port "$SRC_PORT" \
|
||||
--logpath "$LOGF" --logappend --fork --pidfilepath "$SRC_PIDFILE" ${3:-} >/dev/null 2>&1
|
||||
}
|
||||
@@ -327,16 +329,16 @@ fail_3x_keep_progress() { # $1 = importer exit code
|
||||
# CollectionFS + Meteor-Files GridFS attachments/avatars onto the filesystem
|
||||
# (files/attachments, files/avatars). Version only decides HOW we read the source.
|
||||
log "Checking whether mongod 7 can open the existing data ..."
|
||||
# #6458: MongoDB >= 5.0 x86_64 requires AVX. Without it the mongod 7 probe below
|
||||
# can only die with SIGILL, so skip it and go straight to the MongoDB 3.x reader —
|
||||
# the bundled 3.2 tools, the Node importer and FerretDB are all AVX-free, so the
|
||||
# migration itself works fine on such CPUs.
|
||||
HAVE_AVX=1
|
||||
# #6458: MongoDB >= 5.0 x86_64 requires AVX (arm64: ARMv8.2-A). Without them
|
||||
# the mongod 7 probe used to only SIGILL; cpu-exec now runs it through
|
||||
# qemu-user emulation instead, so a modern MongoDB 6/7 database can be READ
|
||||
# for migration on such CPUs too — slow, but this is a one-time read. (The
|
||||
# bundled 3.2 tools, the Node importer and FerretDB need no special features.)
|
||||
MONGOD7_CPU_FEATURES="x86_64=avx,aarch64=atomics"
|
||||
if [ "$(uname -m)" = "x86_64" ] && ! grep -qw avx /proc/cpuinfo 2>/dev/null; then
|
||||
HAVE_AVX=0
|
||||
log "CPU has no AVX support: skipping the mongod 7 probe (it would SIGILL); trying the MongoDB 3.x reader directly."
|
||||
log "CPU has no AVX support: the mongod 7 probe runs through qemu-user emulation (slower)."
|
||||
fi
|
||||
if [ "$HAVE_AVX" = "1" ] && start_mongod "$SNAP/bin/mongod" "" "--wiredTigerCacheSizeGB ${SRC_WT_CACHE_GB}" && ready_via_node; then
|
||||
if start_mongod "$SNAP/bin/mongod" "" "--wiredTigerCacheSizeGB ${SRC_WT_CACHE_GB}" "$MONGOD7_CPU_FEATURES" && ready_via_node; then
|
||||
if [ ! -x "$FERRETDB" ] || [ ! -x "$NODE" ] || [ ! -f "$IMPORTER_MODERN" ]; then
|
||||
fall_back_to_mongodb "Migration tools unavailable; cannot migrate this modern MongoDB to FerretDB."
|
||||
fi
|
||||
|
||||
@@ -196,21 +196,11 @@ trap cleanup EXIT
|
||||
# SQLite — no AVX needed; MongoDB 3.x data is read with the bundled 3.2 tools),
|
||||
# with a counter so a persistently failing migration cannot ping-pong forever.
|
||||
MONGOD_FAIL_COUNT_FILE="${SNAP_COMMON}/.mongod-start-failures"
|
||||
handle_mongod_start_failure() {
|
||||
local rc="$1"
|
||||
# A COMPLETED FerretDB migration exists (marker + non-empty SQLite): switch the
|
||||
# snap to it and exit. (A PARTIAL migration — SQLite present but no marker — is
|
||||
# NOT switched to.) Returns 1 when there is no completed migration.
|
||||
switch_to_completed_ferretdb() {
|
||||
local svc="${SNAP_INSTANCE_NAME:-${SNAP_NAME:-wekan}}"
|
||||
echo "ERROR: mongod failed to start (exit code ${rc})."
|
||||
if [ "$rc" -eq 132 ]; then
|
||||
echo "Exit 132 = SIGILL (Illegal instruction): MongoDB 5.0+ x86_64 binaries require a CPU"
|
||||
echo "with AVX support, and this CPU has none — the bundled MongoDB 7 can never run here."
|
||||
echo "FerretDB (pure Go + SQLite) has no AVX requirement and is the supported fallback."
|
||||
fi
|
||||
echo "Last mongod log lines:"
|
||||
tail -n 5 "$MONGO_LOG_FILE" 2>/dev/null || true
|
||||
|
||||
# A COMPLETED FerretDB migration exists (marker + non-empty SQLite): the snap
|
||||
# should be running FerretDB anyway — switch now instead of crash-looping.
|
||||
# (A PARTIAL migration — SQLite present but no marker — is NOT switched to.)
|
||||
if [ -f "$SNAP_COMMON/.migration-to-ferretdb-done" ] && \
|
||||
bash "$SNAP/bin/ferretdb-has-data" "$SNAP_COMMON/files/db" 2>/dev/null; then
|
||||
echo "A completed FerretDB migration exists; switching to database=ferretdb."
|
||||
@@ -221,6 +211,21 @@ handle_mongod_start_failure() {
|
||||
snapctl stop --disable "${svc}.mongodb" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
handle_mongod_start_failure() {
|
||||
local rc="$1"
|
||||
echo "ERROR: mongod failed to start (exit code ${rc})."
|
||||
if [ "$rc" -eq 132 ]; then
|
||||
echo "Exit 132 = SIGILL (Illegal instruction): MongoDB 5.0+ x86_64 binaries require a CPU"
|
||||
echo "with AVX support (arm64: ARMv8.2-A), and this CPU has none — and no working"
|
||||
echo "qemu-user was available to emulate it (see cpu-exec in the log above)."
|
||||
echo "FerretDB (pure Go + SQLite) has no such CPU requirements and is the supported fallback."
|
||||
fi
|
||||
echo "Last mongod log lines:"
|
||||
tail -n 5 "$MONGO_LOG_FILE" 2>/dev/null || true
|
||||
|
||||
switch_to_completed_ferretdb || true
|
||||
|
||||
local fails=0
|
||||
[ -f "$MONGOD_FAIL_COUNT_FILE" ] && fails="$(cat "$MONGOD_FAIL_COUNT_FILE" 2>/dev/null || echo 0)"
|
||||
@@ -246,11 +251,20 @@ handle_mongod_start_failure() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# #6458: pre-flight AVX check — without AVX, starting mongod below would only
|
||||
# produce the SIGILL crash loop, so fail over right away with a clear message.
|
||||
# #6458: CPU features the bundled MongoDB 7 needs: AVX on x86_64 (missing on
|
||||
# old CPUs and on hypervisors/sandboxes that mask it — the #6458 report runs
|
||||
# inside QEMU without AVX), ARMv8.2-A on arm64 ('atomics'/LSE is the practical
|
||||
# /proc/cpuinfo proxy; Raspberry Pi 4 and older lack it). cpu-exec runs mongod
|
||||
# directly when the CPU has them and through qemu-user emulation when not —
|
||||
# the general mechanism replacing the old amd64-only PATH wrappers that every
|
||||
# absolute-path invocation bypassed.
|
||||
MONGOD_CPU_FEATURES="x86_64=avx,aarch64=atomics"
|
||||
CPU_EXEC="$SNAP/bin/cpu-exec"
|
||||
if [ "$(uname -m)" = "x86_64" ] && ! grep -qw avx /proc/cpuinfo 2>/dev/null; then
|
||||
echo "ERROR: This x86_64 CPU has no AVX support; MongoDB 7 would exit with SIGILL."
|
||||
handle_mongod_start_failure 132
|
||||
echo "NOTE: This x86_64 CPU has no AVX. MongoDB 7 will run through qemu-user emulation (slower)."
|
||||
echo "The native-speed alternative is FerretDB: snap run wekan.migrate"
|
||||
# If the FerretDB migration already completed, prefer it over emulation.
|
||||
switch_to_completed_ferretdb || true
|
||||
fi
|
||||
|
||||
# Check if MongoDB is already running
|
||||
@@ -283,7 +297,7 @@ echo "Replica set member host: ${RS_MEMBER_HOST}"
|
||||
# MongoDB instances from other parallel snap installs.
|
||||
TEMP_PID_FILE="${SNAP_COMMON}/mongod-temp.pid"
|
||||
echo "Starting MongoDB temporarily to check replica set configuration..."
|
||||
$SNAP/bin/mongod \
|
||||
bash "$CPU_EXEC" --features "$MONGOD_CPU_FEATURES" $SNAP/bin/mongod \
|
||||
--dbpath="$MONGO_DATA_DIR" \
|
||||
--logpath="$MONGO_LOG_FILE" \
|
||||
--logappend $BIND_OPTIONS \
|
||||
@@ -294,9 +308,11 @@ $SNAP/bin/mongod \
|
||||
--replSet rs0 --oplogSize 20480 --fork \
|
||||
--pidfilepath="$TEMP_PID_FILE" \
|
||||
|| handle_mongod_start_failure $?
|
||||
# ^ #6458/#6466: with --fork, mongod's parent only returns 0 once the child is up;
|
||||
# a SIGILL (no AVX) or "data files too recent" (3.x data) failure returns non-zero
|
||||
# and previously fell through to ~10 minutes of pinging a dead port, forever.
|
||||
# ^ #6458/#6466: cpu-exec runs mongod through qemu-user when the CPU lacks the
|
||||
# features it needs (AVX / ARMv8.2-A). And with --fork, mongod's parent only
|
||||
# returns 0 once the child is up; a SIGILL (no AVX, no qemu) or "data files too
|
||||
# recent" (3.x data) failure returns non-zero and previously fell through to
|
||||
# ~10 minutes of pinging a dead port, forever.
|
||||
|
||||
# Wait for MongoDB to accept connections (up to 30 s). db-eval uses the bundled
|
||||
# Node.js + `mongodb` driver; directConnection reaches this node before the
|
||||
@@ -358,7 +374,7 @@ sleep 1
|
||||
# incompatible data files — is caught below instead of snapd blindly restarting
|
||||
# this service into the same crash forever)
|
||||
_mongod_start_ts=$(date +%s)
|
||||
$SNAP/bin/mongod \
|
||||
bash "$CPU_EXEC" --features "$MONGOD_CPU_FEATURES" $SNAP/bin/mongod \
|
||||
--dbpath="$MONGO_DATA_DIR" \
|
||||
--logpath="$MONGO_LOG_FILE" \
|
||||
--logappend $BIND_OPTIONS \
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issue #5282: deleting an
|
||||
// attachment could log a client-side "Removed nonexistent document" exception
|
||||
// even though the delete itself succeeded — the same publication-churn class
|
||||
// as the fixed #3252 for comments and checklists, whose guards attachments
|
||||
// never got.
|
||||
// Run: node tests/attachmentDeleteGuard.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const attachments = read('client/components/cards/attachments.js');
|
||||
const comments = read('client/components/activities/comments.js');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
test('attachment delete only removes when the doc is still in the local cache', () => {
|
||||
assert.ok(
|
||||
/if \(this\._id && ReactiveCache\.getAttachment\(this\._id\)\) \{\s*\n\s*await Attachments\.removeAsync\(this\._id\);/.test(attachments),
|
||||
'removeAsync must be guarded by a Minimongo presence check',
|
||||
);
|
||||
});
|
||||
|
||||
test('negative: no unguarded removeAsync remains in the delete handler', () => {
|
||||
const handler = attachments.match(/'click \.js-confirm-delete'[\s\S]*?\n \}\),/);
|
||||
assert.ok(handler, 'delete handler found');
|
||||
const body = handler[0];
|
||||
const removes = body.match(/Attachments\.removeAsync/g) || [];
|
||||
assert.strictEqual(removes.length, 1, 'exactly one remove call, inside the guard');
|
||||
});
|
||||
|
||||
test('the sibling #3252 guard for comments is still in place (same bug class)', () => {
|
||||
assert.ok(/CardComments\.findOne\(commentId\)/.test(comments));
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,112 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issue #6455: the "You have an
|
||||
// unsaved description" warning could never be cleared by pressing Save.
|
||||
// Run: node tests/cardDescriptionDraft.test.cjs
|
||||
//
|
||||
// Two bugs, both guarded here:
|
||||
// 1. Closing the description editor only AVOIDED ADDING a draft when the text
|
||||
// matched the saved description; it never REMOVED a pre-existing
|
||||
// unsaved-edits record — so once the warning appeared, "View it" -> Save
|
||||
// could not clear it, only Discard could.
|
||||
// 2. The comparison matched a per-line-whitespace-stripped draft against the
|
||||
// RAW stored description (or null when empty), so descriptions with
|
||||
// Markdown " \n" hard-breaks re-created a phantom draft on every save.
|
||||
//
|
||||
// The behavioral part extracts the actual `normalize` arrow function from the
|
||||
// component source and exercises it, so the test fails if the normalization
|
||||
// semantics change; the structural part asserts the reset paths exist.
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const cardDetails = read('client/components/cards/cardDetails.js');
|
||||
const cardDescription = read('client/components/cards/cardDescription.js');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- Behavioral: the shared normalization used on BOTH sides ----------------
|
||||
|
||||
const normalizeSrc = cardDetails.match(
|
||||
/const normalize = (\(s\) => .*);/,
|
||||
);
|
||||
assert.ok(normalizeSrc, 'normalize arrow function found in cardDetails.js');
|
||||
// eslint-disable-next-line no-new-func
|
||||
const normalize = new Function(`return ${normalizeSrc[1]};`)();
|
||||
|
||||
test('normalize strips per-line trailing whitespace (Markdown hard breaks)', () => {
|
||||
assert.strictEqual(normalize('line one \nline two\t\nend'), 'line one\nline two\nend');
|
||||
});
|
||||
|
||||
test('normalize maps null/undefined (empty description) to empty string', () => {
|
||||
assert.strictEqual(normalize(null), '');
|
||||
assert.strictEqual(normalize(undefined), '');
|
||||
});
|
||||
|
||||
test('normalize makes draft and hard-break description compare EQUAL (#6455 bug 2)', () => {
|
||||
const stored = 'first line \nsecond'; // what setDescription saved (raw)
|
||||
const draftFromTextarea = 'first line\nsecond'; // what _getValue() produces
|
||||
assert.strictEqual(normalize(stored), normalize(draftFromTextarea));
|
||||
});
|
||||
|
||||
test('negative: genuinely different drafts still compare UNEQUAL', () => {
|
||||
assert.notStrictEqual(normalize('alpha'), normalize('beta'));
|
||||
// internal (non-trailing) whitespace is content, not noise
|
||||
assert.notStrictEqual(normalize('a b'), normalize('ab'));
|
||||
});
|
||||
|
||||
// --- Structural: the reset paths that were missing -------------------------
|
||||
|
||||
test('closing with a matching draft RESETS the record (bug 1 fix present)', () => {
|
||||
// The else-branch reset inside _close(); before #6455 only the set() existed.
|
||||
const closeBody = cardDetails.match(/this\._close = \(isReset = false\) => \{[\s\S]*?\n \};/);
|
||||
assert.ok(closeBody, '_close found');
|
||||
assert.ok(
|
||||
closeBody[0].includes('UnsavedEdits.set(this._getUnsavedEditKey()'),
|
||||
'still saves a differing draft',
|
||||
);
|
||||
assert.ok(
|
||||
closeBody[0].includes('UnsavedEdits.reset(this._getUnsavedEditKey())'),
|
||||
'_close must reset the draft when it matches the saved description',
|
||||
);
|
||||
});
|
||||
|
||||
test('both sides of the _close comparison are normalized the same way', () => {
|
||||
assert.ok(
|
||||
/draft !== normalize\(card\.getDescription\(\)\)/.test(cardDetails),
|
||||
'comparison must normalize getDescription(), not compare it raw',
|
||||
);
|
||||
});
|
||||
|
||||
test('successful description save clears the draft explicitly (cardDetails.js)', () => {
|
||||
const submit = cardDetails.match(/async 'submit \.js-card-description'[\s\S]*?\n \},/);
|
||||
assert.ok(submit, 'submit handler found');
|
||||
assert.ok(submit[0].includes('await card.setDescription(description);'));
|
||||
assert.ok(
|
||||
submit[0].includes("UnsavedEdits.reset({ fieldName: 'cardDescription', docId: card._id })"),
|
||||
'submit must remove the unsaved-edits record after a successful save',
|
||||
);
|
||||
});
|
||||
|
||||
test('successful description save clears the draft explicitly (cardDescription.js)', () => {
|
||||
assert.ok(cardDescription.includes("import { UnsavedEdits } from '/client/lib/unsavedEdits';"));
|
||||
assert.ok(
|
||||
/await this\.setDescription\(description\);[\s\S]*?UnsavedEdits\.reset\(\{[\s\S]*?fieldName: 'cardComment'/.test(cardDescription) === false,
|
||||
'sanity: description form must reset the cardDescription draft, not cardComment',
|
||||
);
|
||||
assert.ok(
|
||||
/UnsavedEdits\.reset\(\{ fieldName: 'cardDescription', docId: this\._id \}\)/.test(cardDescription),
|
||||
'descriptionForm submit must remove the draft record',
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issue #5547: a comment being
|
||||
// written was lost when the card was closed or a click landed outside it.
|
||||
// Run: node tests/commentDraft.test.cjs
|
||||
//
|
||||
// History this test protects: the comment-draft machinery existed since 2015
|
||||
// (the form prefills from {{getUnsavedValue 'cardComment' ...}}), but the
|
||||
// escape handler that SAVED the draft was gated on a commentFormIsOpen flag
|
||||
// whose only setter was removed in 2019 (commit 3b3950369) — because that
|
||||
// handler also CLEARED the visible textarea on every outside click. Since
|
||||
// then, nothing saved the draft at all. The #5547 fix:
|
||||
// - saves the draft continuously (debounced) while typing,
|
||||
// - flushes a pending save when the form is torn down (card closed, route
|
||||
// change),
|
||||
// - clears the draft on successful submit (and cancels a pending save so a
|
||||
// late debounce cannot resurrect it),
|
||||
// - re-arms the escape handler (focus sets the flag) WITHOUT re-introducing
|
||||
// the 2019 "text disappears on outside click" bug (no input clearing).
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const src = fs.readFileSync(
|
||||
path.join(repoRoot, 'client/components/activities/comments.js'),
|
||||
'utf8',
|
||||
);
|
||||
const jade = fs.readFileSync(
|
||||
path.join(repoRoot, 'client/components/activities/comments.jade'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- POSITIVE: the draft lifecycle -------------------------------------------
|
||||
|
||||
test('typing schedules a (debounced) draft save', () => {
|
||||
assert.ok(/'input \.js-new-comment-input'\(evt\) \{\s*\n\s*scheduleCommentDraftSave\(/.test(src));
|
||||
});
|
||||
|
||||
test('form teardown flushes a pending draft save (card closed, route change)', () => {
|
||||
const onDestroyed = src.match(/Template\.commentForm\.onDestroyed\(function \(\) \{[\s\S]*?\n\}\);/);
|
||||
assert.ok(onDestroyed, 'onDestroyed found');
|
||||
assert.ok(onDestroyed[0].includes('flushCommentDraftSave();'));
|
||||
});
|
||||
|
||||
test('focus arms the escape handler again (the flag lost its setter in 2019)', () => {
|
||||
assert.ok(/'focus \.js-new-comment-input'\(\) \{\s*\n\s*commentFormIsOpen\.set\(true\);/.test(src));
|
||||
});
|
||||
|
||||
test('the form prefills from the stored draft', () => {
|
||||
assert.ok(jade.includes("{{getUnsavedValue 'cardComment' currentCard._id}}"));
|
||||
});
|
||||
|
||||
test('submit clears the draft AND cancels any pending debounced save', () => {
|
||||
const submit = src.match(/'submit \.js-new-comment-form'[\s\S]*?\n \},/);
|
||||
assert.ok(submit, 'submit handler found');
|
||||
assert.ok(submit[0].includes('cancelCommentDraftSave();'),
|
||||
'a debounce still in flight must not resurrect the submitted comment as a draft');
|
||||
assert.ok(/UnsavedEdits\.reset\(\{\s*\n?\s*fieldName: 'cardComment',/.test(submit[0]),
|
||||
'submit must remove the stored draft');
|
||||
});
|
||||
|
||||
// --- NEGATIVE: the 2019 regression must NOT come back ------------------------
|
||||
|
||||
test("escape handler no longer clears the visible text (2019's 'text disappears')", () => {
|
||||
const escapeBlock = src.match(/EscapeActions\.register\(\s*'inlinedForm',[\s\S]*?\n\);/);
|
||||
assert.ok(escapeBlock, 'escape registration found');
|
||||
assert.ok(!escapeBlock[0].includes('resetCommentInput('),
|
||||
'escape must not clear the comment input — only blur and save the draft');
|
||||
assert.ok(escapeBlock[0].includes('commentInput.blur();'));
|
||||
assert.ok(escapeBlock[0].includes('commentFormIsOpen.set(false);'),
|
||||
'escape must disarm the flag so the next Escape press can close the card');
|
||||
});
|
||||
|
||||
test('escape handler still saves a non-empty draft and resets an empty one', () => {
|
||||
const escapeBlock = src.match(/EscapeActions\.register\(\s*'inlinedForm',[\s\S]*?\n\);/);
|
||||
assert.ok(escapeBlock[0].includes('UnsavedEdits.set(draftKey, draft);'));
|
||||
assert.ok(escapeBlock[0].includes('UnsavedEdits.reset(draftKey);'));
|
||||
});
|
||||
|
||||
test('an empty draft never persists (write helper resets instead of saving)', () => {
|
||||
const writer = src.match(/function commentDraftWrite\(\{ cardId, value \}\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(writer, 'commentDraftWrite found');
|
||||
assert.ok(/if \(\(value \|\| ''\)\.trim\(\)\)/.test(writer[0]),
|
||||
'whitespace-only text must reset the draft, not store it');
|
||||
assert.ok(writer[0].includes('UnsavedEdits.reset(draftKey);'));
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node BEHAVIORAL test for snap-src/bin/cpu-exec (#6458): the general
|
||||
// helper that runs a binary directly when the CPU has the features it needs
|
||||
// and through qemu-user emulation when it does not — on all platforms, for
|
||||
// all scripts (Snap, bundle .zip, Docker, Sandstorm).
|
||||
// Run: node tests/cpuExec.test.cjs (needs bash — CI runners are Linux)
|
||||
//
|
||||
// The script honors WEKAN_CPUINFO (fake /proc/cpuinfo) exactly so it can be
|
||||
// tested like this without caring about the build machine's real CPU.
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const CPU_EXEC = path.join(repoRoot, 'snap-src/bin/cpu-exec');
|
||||
const ARCH = os.arch() === 'x64' ? 'x86_64' : os.arch() === 'arm64' ? 'aarch64' : os.machine ? os.machine() : 'x86_64';
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-exec-test-'));
|
||||
const binDir = path.join(tmp, 'bin');
|
||||
fs.mkdirSync(binDir);
|
||||
const cpuinfoWith = path.join(tmp, 'cpuinfo-with');
|
||||
const cpuinfoWithout = path.join(tmp, 'cpuinfo-without');
|
||||
fs.writeFileSync(cpuinfoWith, 'flags\t\t: fpu vme avx sse2\nFeatures\t: fp asimd atomics\n');
|
||||
fs.writeFileSync(cpuinfoWithout, 'flags\t\t: fpu vme sse2\nFeatures\t: fp asimd\n');
|
||||
// Fake same-arch qemu-user that marks its involvement.
|
||||
const fakeQemu = path.join(binDir, `qemu-${ARCH}`);
|
||||
fs.writeFileSync(fakeQemu, '#!/bin/bash\necho "QEMU-WRAPPED: $@"\n');
|
||||
fs.chmodSync(fakeQemu, 0o755);
|
||||
|
||||
function run(args, env = {}, withQemuOnPath = false) {
|
||||
const PATH = withQemuOnPath ? `${binDir}:/usr/bin:/bin` : '/usr/bin:/bin';
|
||||
return execFileSync('bash', [CPU_EXEC, ...args], {
|
||||
env: { PATH, ...env },
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
test('feature present: the binary runs directly', () => {
|
||||
const out = run(['--features', `${ARCH}=avx`, 'echo', 'direct'], { WEKAN_CPUINFO: cpuinfoWith });
|
||||
assert.strictEqual(out.trim(), 'direct');
|
||||
});
|
||||
|
||||
test('feature missing: the binary runs through qemu-user', () => {
|
||||
const out = run(['--features', `${ARCH}=avx`, 'echo', 'hello'], { WEKAN_CPUINFO: cpuinfoWithout }, true);
|
||||
assert.strictEqual(out.trim(), 'QEMU-WRAPPED: echo hello');
|
||||
});
|
||||
|
||||
test('no declared features: plain exec (safe for every binary)', () => {
|
||||
const out = run(['echo', 'plain'], { WEKAN_CPUINFO: cpuinfoWithout });
|
||||
assert.strictEqual(out.trim(), 'plain');
|
||||
});
|
||||
|
||||
test('negative: feature missing AND no qemu — still runs, with a clear error', () => {
|
||||
// Running directly (probably to a SIGILL) beats silently doing nothing:
|
||||
// the real failure must surface in the logs.
|
||||
const out = execFileSync('bash', [CPU_EXEC, '--features', `${ARCH}=avx`, 'echo', 'ran-anyway'], {
|
||||
env: { PATH: '/usr/bin:/bin', WEKAN_CPUINFO: cpuinfoWithout },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const stderr = execFileSync('bash', ['-c', `"${CPU_EXEC}" --features "${ARCH}=avx" true 2>&1 1>/dev/null || true`], {
|
||||
env: { PATH: '/usr/bin:/bin', WEKAN_CPUINFO: cpuinfoWithout },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(out.trim(), 'ran-anyway');
|
||||
assert.ok(/cpu-exec: ERROR: .*lacks avx.*no qemu-user/.test(stderr), `stderr explains: ${stderr}`);
|
||||
});
|
||||
|
||||
test("negative: another architecture's requirements are ignored", () => {
|
||||
const out = run(['--features', 'someotherarch=avx', 'echo', 'ignored'], { WEKAN_CPUINFO: cpuinfoWithout });
|
||||
assert.strictEqual(out.trim(), 'ignored');
|
||||
});
|
||||
|
||||
test('WEKAN_REQUIRED_CPU_FEATURES overrides --features', () => {
|
||||
const out = run(
|
||||
['--features', `${ARCH}=featurethatdoesnotexist`, 'echo', 'env-wins'],
|
||||
{ WEKAN_CPUINFO: cpuinfoWith, WEKAN_REQUIRED_CPU_FEATURES: `${ARCH}=avx+sse2` },
|
||||
);
|
||||
assert.strictEqual(out.trim(), 'env-wins');
|
||||
});
|
||||
|
||||
test('multi-feature spec: one missing feature is enough to emulate', () => {
|
||||
const out = run(['--features', `${ARCH}=sse2+avx`, 'echo', 'multi'], { WEKAN_CPUINFO: cpuinfoWithout }, true);
|
||||
assert.strictEqual(out.trim(), 'QEMU-WRAPPED: echo multi');
|
||||
});
|
||||
|
||||
test('WEKAN_QEMU_USER explicit override wins', () => {
|
||||
const out = run(['--features', `${ARCH}=avx`, 'echo', 'via-override'], {
|
||||
WEKAN_CPUINFO: cpuinfoWithout,
|
||||
WEKAN_QEMU_USER: fakeQemu,
|
||||
});
|
||||
assert.strictEqual(out.trim(), 'QEMU-WRAPPED: echo via-override');
|
||||
});
|
||||
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issues #6467/#6468 (WeKan side):
|
||||
// with FerretDB there is no oplog, so Meteor observes every query by POLLING —
|
||||
// and Meteor's defaults re-run every observed query 50 ms after ANY write and
|
||||
// at least every 10 s, which multiplied into hundreds of full queries per
|
||||
// second and pinned FerretDB's CPU at 250-400% for 1-2 users. The snap and the
|
||||
// bundle launcher must default to calmer polling when the database is
|
||||
// FerretDB, while keeping user overrides.
|
||||
// Run: node tests/ferretdbPolling.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const wekanControl = read('snap-src/bin/wekan-control');
|
||||
const startWekan = read('releases/ferretdb/start-wekan.sh');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
test('snap: calm polling defaults exist for the ferretdb database', () => {
|
||||
assert.ok(wekanControl.includes('METEOR_POLLING_THROTTLE_MS=2000'));
|
||||
assert.ok(wekanControl.includes('METEOR_POLLING_INTERVAL_MS=30000'));
|
||||
});
|
||||
|
||||
test('snap: the defaults are scoped to database=ferretdb', () => {
|
||||
const block = wekanControl.match(
|
||||
/if \[ "ferretdb" = "\$\{DATABASE\}" \]; then[\s\S]*?METEOR_POLLING_INTERVAL_MS[\s\S]*?\nfi/,
|
||||
);
|
||||
assert.ok(block, 'polling exports live inside a ferretdb-only guard — MongoDB uses the oplog');
|
||||
});
|
||||
|
||||
test('snap: user-provided values are respected (only set when empty)', () => {
|
||||
assert.ok(/\[ -z "\$\{METEOR_POLLING_THROTTLE_MS\}" \] && export METEOR_POLLING_THROTTLE_MS=2000/.test(wekanControl));
|
||||
assert.ok(/\[ -z "\$\{METEOR_POLLING_INTERVAL_MS\}" \] && export METEOR_POLLING_INTERVAL_MS=30000/.test(wekanControl));
|
||||
});
|
||||
|
||||
test('bundle launcher: same defaults, same override semantics', () => {
|
||||
assert.ok(startWekan.includes('METEOR_POLLING_THROTTLE_MS="${METEOR_POLLING_THROTTLE_MS:-2000}"'));
|
||||
assert.ok(startWekan.includes('METEOR_POLLING_INTERVAL_MS="${METEOR_POLLING_INTERVAL_MS:-30000}"'));
|
||||
});
|
||||
|
||||
test('bundle launcher: defaults only apply when the bundled FerretDB is used', () => {
|
||||
assert.ok(/if \[ "\$want_ferret" = true \]; then\s*\n\s*export METEOR_POLLING_THROTTLE_MS/.test(startWekan),
|
||||
'pointing MONGO_URL at an external MongoDB must keep Meteor defaults');
|
||||
});
|
||||
|
||||
test('negative: the snap does not advertise an oplog URL for ferretdb', () => {
|
||||
// The whole point of the polling defaults: with FerretDB there is no oplog.
|
||||
assert.ok(/\[ "ferretdb" != "\$\{DATABASE\}" \]/.test(wekanControl),
|
||||
'MONGO_OPLOG_URL stays MongoDB-only');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node unit test (no Meteor) for issue #6459: the card "More" menu
|
||||
// rendered completely empty (no Delete option).
|
||||
// Run: node tests/htmljsArrayContent.test.cjs
|
||||
//
|
||||
// Root cause: a WeKan-local vm-sandbox patch in the vendored jade compiler's
|
||||
// htmljs made isConstructedObject(Array) return false (upstream htmljs returns
|
||||
// true), so the HTML.Tag constructor took an ARRAY first argument — the
|
||||
// compiled form of a tag with multi-part inline text like
|
||||
// `label {{_ 'source-board'}}:` (a [TemplateTag, ":"] array) — as the tag's
|
||||
// ATTRIBUTES dictionary instead of its content. At runtime Blaze then found a
|
||||
// template view object inside the attributes and threw "The basic
|
||||
// TransformingVisitor does not support foreign objects in attributes",
|
||||
// killing the whole cardMorePopup render.
|
||||
//
|
||||
// The fix makes the Tag constructor treat an array first argument as content
|
||||
// (like upstream), while keeping the vm-sandbox patch's answer for
|
||||
// isConstructedObject(Array) itself (flattenAttributes depends on it).
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
// The vendored file is a sloppy-mode IIFE that publishes onto a `Package`
|
||||
// global (creating it when missing) — exactly how the jade loader consumes it.
|
||||
require('../npm-packages/meteor-jade-loader/lib/vendor/htmljs.js');
|
||||
const HTML = global.Package.htmljs.HTML;
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- POSITIVE: #6459 — an array first argument is tag CONTENT ---------------
|
||||
|
||||
test('array first argument becomes children, not attrs (the #6459 crash)', () => {
|
||||
// Compiled shape of `label {{_ 'source-board'}}:` — inline text that is an
|
||||
// array of [something-view-like, ':'].
|
||||
const viewLike = { foo: 'templateTagPlaceholder' };
|
||||
const tag = HTML.LABEL([viewLike, ':'], HTML.SELECT());
|
||||
assert.strictEqual(tag.attrs, null, 'attrs must stay empty (prototype default)');
|
||||
assert.strictEqual(tag.children.length, 2);
|
||||
assert.ok(Array.isArray(tag.children[0]), 'first child is the inline-text array');
|
||||
assert.strictEqual(tag.children[0][1], ':');
|
||||
});
|
||||
|
||||
test('array-only tag keeps its content renderable', () => {
|
||||
const tag = HTML.LABEL(['a', 'b']);
|
||||
assert.strictEqual(tag.attrs, null);
|
||||
assert.deepStrictEqual(tag.children[0], ['a', 'b']);
|
||||
});
|
||||
|
||||
// --- NEGATIVE: everything that must keep working exactly as before ----------
|
||||
|
||||
test('plain object first argument is still the attributes dictionary', () => {
|
||||
const tag = HTML.LABEL({ class: 'nice' }, 'text');
|
||||
assert.deepStrictEqual(tag.attrs, { class: 'nice' });
|
||||
assert.deepStrictEqual(tag.children, ['text']);
|
||||
});
|
||||
|
||||
test('HTML.Attrs first argument is still unwrapped into attrs', () => {
|
||||
const tag = HTML.LABEL(HTML.Attrs({ id: 'x' }), 'text');
|
||||
assert.deepStrictEqual(tag.attrs, { id: 'x' });
|
||||
assert.deepStrictEqual(tag.children, ['text']);
|
||||
});
|
||||
|
||||
test('a constructed object (another tag) first argument is content', () => {
|
||||
const inner = HTML.SPAN('y');
|
||||
const tag = HTML.LABEL(inner, 'text');
|
||||
assert.strictEqual(tag.attrs, null);
|
||||
assert.strictEqual(tag.children[0], inner);
|
||||
});
|
||||
|
||||
test('vm-sandbox patch stays: isConstructedObject(Array) is false', () => {
|
||||
// flattenAttributes relies on arrays NOT being "constructed objects"; only
|
||||
// the Tag constructor needed the array special-case.
|
||||
assert.strictEqual(HTML.isConstructedObject([1, 2]), false);
|
||||
});
|
||||
|
||||
test('no-argument tags still work', () => {
|
||||
const tag = HTML.LABEL();
|
||||
assert.strictEqual(tag.attrs, null);
|
||||
assert.strictEqual(tag.children.length, 0);
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issue #6472: rules with
|
||||
// actionType moveCardToTop / moveCardToBottom did nothing.
|
||||
// Run: node tests/ruleMoveAction.test.cjs
|
||||
//
|
||||
// Five silent bugs (the activity hook swallows rule-action errors):
|
||||
// 1. an unresolved destination list crashed on list.cardsUnfiltered — now the
|
||||
// action falls back to the card's CURRENT list (staying on the card's own
|
||||
// board and swimlane),
|
||||
// 2. the classic wizard's generic "move to top/bottom" stored listTitle,
|
||||
// which the rule engine never reads (it reads listName),
|
||||
// 3. an empty destination made Math.min()/Math.max() of nothing write a
|
||||
// corrupt sort of +/-Infinity,
|
||||
// 4. the rules JSON/CSV import created rules with raw client inserts that
|
||||
// the board-admin-only allow rules reject into minimongo limbo — and a
|
||||
// trigger document missing a matching field (e.g. userId) can never
|
||||
// match, since the matcher queries every field with {$in: [value, '*']},
|
||||
// 5. rules.createRule let an empty boardId ('') from a not-yet-loaded board
|
||||
// selector override the real board.
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const rulesHelper = read('server/rulesHelper.js');
|
||||
const boardActions = read('client/components/rules/actions/boardActions.js');
|
||||
const importExport = read('client/components/rules/rulesImportExport.js');
|
||||
const rulesButton = read('server/rulesButton.js');
|
||||
const lists = read('models/lists.js');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- Bug 1: unresolved list must not crash ----------------------------------
|
||||
|
||||
test('unresolved listName falls back to the card’s current list', () => {
|
||||
assert.ok(rulesHelper.includes('let fellBackToCardList = false;'));
|
||||
assert.ok(/if \(!list\) \{[\s\S]*?list = await card\.list\(\);[\s\S]*?fellBackToCardList = true;/.test(rulesHelper));
|
||||
});
|
||||
|
||||
test('fallback move stays on the card’s own board and swimlane', () => {
|
||||
assert.ok(/if \(fellBackToCardList\) \{\s*\n\s*destBoardId = list\.boardId;\s*\n\s*swimlaneId = card\.swimlaneId;/.test(rulesHelper));
|
||||
});
|
||||
|
||||
test('negative: the crashing unguarded pattern is gone', () => {
|
||||
// Old code: Math.min(...(await list.cardsUnfiltered(swimlaneId)).map(...))
|
||||
// dereferenced `list` (possibly undefined) and spread possibly-empty sorts.
|
||||
assert.ok(!/Math\.min\(\s*\.\.\.\(await list\.cardsUnfiltered/.test(rulesHelper));
|
||||
assert.ok(!/Math\.max\(\s*\.\.\.\(await list\.cardsUnfiltered/.test(rulesHelper));
|
||||
});
|
||||
|
||||
// --- Bug 3: empty destination must not write sort +/-Infinity ---------------
|
||||
|
||||
test('destination sorts are filtered to finite numbers with a 0 fallback', () => {
|
||||
assert.ok(rulesHelper.includes('.filter(Number.isFinite)'));
|
||||
assert.ok(rulesHelper.includes('destSorts.length ? Math.min(...destSorts) : 0'));
|
||||
assert.ok(rulesHelper.includes('destSorts.length ? Math.max(...destSorts) : 0'));
|
||||
});
|
||||
|
||||
test('behavior: the guarded min/max never produces Infinity', () => {
|
||||
// Same expressions as the fix, exercised directly.
|
||||
const top = (destSorts) => (destSorts.length ? Math.min(...destSorts) : 0) - 1;
|
||||
const bottom = (destSorts) => (destSorts.length ? Math.max(...destSorts) : 0) + 1;
|
||||
assert.strictEqual(top([]), -1);
|
||||
assert.strictEqual(bottom([]), 1);
|
||||
assert.strictEqual(top([5, 2, 9]), 1);
|
||||
assert.strictEqual(bottom([5, 2, 9]), 10);
|
||||
// negative: the OLD expressions did produce Infinity on empty input
|
||||
assert.strictEqual(Math.min(...[]), Infinity);
|
||||
assert.strictEqual(Math.max(...[]), -Infinity);
|
||||
});
|
||||
|
||||
// --- Bug 2: wizard field names -----------------------------------------------
|
||||
|
||||
test('generic move wizard stores listName/swimlaneName wildcards', () => {
|
||||
assert.ok(/listName: '\*',\s*\n\s*swimlaneName: '\*',/.test(boardActions));
|
||||
});
|
||||
|
||||
test('negative: no action is created with the unread listTitle field', () => {
|
||||
// (the word may appear in comments; only the actual object property matters)
|
||||
assert.ok(!/^\s*listTitle: '\*',$/m.test(boardActions));
|
||||
});
|
||||
|
||||
test('rule engine treats a MISSING listName like the * wildcard', () => {
|
||||
assert.ok(/if \(action\.listName === '\*' \|\| !action\.listName\) \{/.test(rulesHelper),
|
||||
'old wizard rules with only listTitle stored must still work');
|
||||
});
|
||||
|
||||
// --- Bug 4: JSON/CSV import ---------------------------------------------------
|
||||
|
||||
test('import creates rules via the rules.createRule server method', () => {
|
||||
assert.ok(/Meteor\.call\(\s*\n?\s*'rules\.createRule',/.test(importExport));
|
||||
assert.ok(!/Triggers\.insert\(/.test(importExport), 'no raw client trigger inserts');
|
||||
assert.ok(!/Actions\.insert\(/.test(importExport), 'no raw client action inserts');
|
||||
assert.ok(!/Rules\.insert\(/.test(importExport), 'no raw client rule inserts');
|
||||
});
|
||||
|
||||
// Extract and exercise the real normalizeTrigger from the import module.
|
||||
const fieldsSrc = importExport.match(/const TRIGGER_MATCHING_FIELDS = \[[\s\S]*?\];/);
|
||||
const fnSrc = importExport.match(/function normalizeTrigger\(trigger\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(fieldsSrc && fnSrc, 'normalizeTrigger and its field list found');
|
||||
// eslint-disable-next-line no-new-func
|
||||
const normalizeTrigger = new Function(
|
||||
`${fieldsSrc[0]}\n${fnSrc[0]}\nreturn normalizeTrigger;`,
|
||||
)();
|
||||
|
||||
test('import defaults missing trigger matching fields to the * wildcard', () => {
|
||||
const out = normalizeTrigger({ activityType: 'completeChecklist', checklistName: 'Done' });
|
||||
assert.strictEqual(out.userId, '*', 'a JSON trigger without userId must still match');
|
||||
assert.strictEqual(out.listName, '*');
|
||||
assert.strictEqual(out.swimlaneName, '*');
|
||||
assert.strictEqual(out.checklistName, 'Done', 'explicit values are kept');
|
||||
assert.strictEqual(out.activityType, 'completeChecklist');
|
||||
});
|
||||
|
||||
test('negative: explicit trigger values are never overwritten', () => {
|
||||
const out = normalizeTrigger({ activityType: 'createCard', userId: 'abc', cardTitle: 'T' });
|
||||
assert.strictEqual(out.userId, 'abc');
|
||||
assert.strictEqual(out.cardTitle, 'T');
|
||||
});
|
||||
|
||||
test('empty-string trigger values become the wildcard too', () => {
|
||||
const out = normalizeTrigger({ activityType: 'createCard', userId: '' });
|
||||
assert.strictEqual(out.userId, '*');
|
||||
});
|
||||
|
||||
// --- Bug 5: empty boardId in rules.createRule --------------------------------
|
||||
|
||||
test('rules.createRule falls back when the action posts an empty boardId', () => {
|
||||
assert.ok(rulesButton.includes('if (!actionDoc.boardId) actionDoc.boardId = boardId;'));
|
||||
});
|
||||
|
||||
// --- Companion fix: server-side orphaned-cards fallback actually resolves ----
|
||||
|
||||
test('List.orphanedCardsSwimlaneIds resolves the async server lookup', () => {
|
||||
const fn = lists.match(/orphanedCardsSwimlaneIds\(swimlaneId\) \{[\s\S]*?\n \},/);
|
||||
assert.ok(fn, 'orphanedCardsSwimlaneIds found');
|
||||
assert.ok(fn[0].includes("typeof swimlanes.then === 'function'"),
|
||||
'server Promise from ReactiveCache.getSwimlanes must be awaited, not read synchronously');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,132 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node regression guard (no Meteor) for issues #6458 and #6466: the
|
||||
// WeKan snap served "502 Bad Gateway" forever when mongod could not start
|
||||
// (SIGILL on CPUs without AVX; MongoDB 3.x data files after a failed
|
||||
// migration), and a migration finishing with a few per-item errors ("Avatars
|
||||
// Errors") deleted the fully-copied FerretDB database.
|
||||
// Run: node tests/snapMigrationRecovery.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const mongodbControl = read('snap-src/bin/mongodb-control');
|
||||
const migrationControl = read('snap-src/bin/migration-control');
|
||||
const importer3 = read('snap-src/bin/migrate-mongo3-to-ferretdb.mjs');
|
||||
const importerModern = read('releases/migrate-mongodb-to-ferretdb.mjs');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- #6458: mongod start failures get a real outcome --------------------------
|
||||
|
||||
test('mongodb-control has an AVX pre-flight (MongoDB 5+ SIGILLs without AVX)', () => {
|
||||
assert.ok(mongodbControl.includes('grep -qw avx /proc/cpuinfo'));
|
||||
assert.ok(/uname -m.*x86_64/.test(mongodbControl), 'AVX check is x86_64-only');
|
||||
});
|
||||
|
||||
test('mongod runs through cpu-exec with its CPU feature requirements', () => {
|
||||
assert.ok(mongodbControl.includes('MONGOD_CPU_FEATURES="x86_64=avx,aarch64=atomics"'));
|
||||
const wrapped = mongodbControl.match(/bash "\$CPU_EXEC" --features "\$MONGOD_CPU_FEATURES" \$SNAP\/bin\/mongod/g) || [];
|
||||
assert.strictEqual(wrapped.length, 2, 'both the temp fork and the final start use cpu-exec');
|
||||
assert.ok(!/^\$SNAP\/bin\/mongod \\$/m.test(mongodbControl), 'no bare absolute-path mongod start remains');
|
||||
});
|
||||
|
||||
test('the temporary mongod fork exit status is checked', () => {
|
||||
assert.ok(mongodbControl.includes('|| handle_mongod_start_failure $?'));
|
||||
});
|
||||
|
||||
test('the final mongod start is no longer exec-and-forget', () => {
|
||||
assert.ok(!/exec \$SNAP\/bin\/mongod/.test(mongodbControl),
|
||||
'exec would let snapd blindly restart into the same crash forever');
|
||||
assert.ok(mongodbControl.includes('_mongod_rc=$?'));
|
||||
assert.ok(/handle_mongod_start_failure \$_mongod_rc/.test(mongodbControl));
|
||||
});
|
||||
|
||||
test('failure handler switches to a COMPLETED FerretDB migration only', () => {
|
||||
const switcher = mongodbControl.match(/switch_to_completed_ferretdb\(\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(switcher, 'switch_to_completed_ferretdb found');
|
||||
assert.ok(switcher[0].includes('.migration-to-ferretdb-done'),
|
||||
'must require the completion marker');
|
||||
assert.ok(switcher[0].includes('ferretdb-has-data'),
|
||||
'must require a non-empty SQLite');
|
||||
assert.ok(switcher[0].includes('snapctl set database=ferretdb'));
|
||||
const handler = mongodbControl.match(/handle_mongod_start_failure\(\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(handler, 'handler found');
|
||||
assert.ok(handler[0].includes('switch_to_completed_ferretdb'),
|
||||
'the failure handler prefers the completed migration');
|
||||
});
|
||||
|
||||
test('failure handler re-runs the migration with a bounded attempt counter', () => {
|
||||
const handler = mongodbControl.match(/handle_mongod_start_failure\(\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(handler[0].includes('exec bash "$SNAP/bin/migration-control"'));
|
||||
assert.ok(/-le 3/.test(handler[0]), 'at most 3 automatic attempts');
|
||||
assert.ok(mongodbControl.includes('MONGOD_FAIL_COUNT_FILE='));
|
||||
});
|
||||
|
||||
test('the failure counter is cleared once mongod answers', () => {
|
||||
assert.ok(/rm -f "\$MONGOD_FAIL_COUNT_FILE"/.test(mongodbControl));
|
||||
});
|
||||
|
||||
test('migration-control probes mongod 7 through cpu-exec (qemu on non-AVX CPUs)', () => {
|
||||
assert.ok(migrationControl.includes('MONGOD7_CPU_FEATURES="x86_64=avx,aarch64=atomics"'));
|
||||
assert.ok(/start_mongod "\$SNAP\/bin\/mongod" "" "--wiredTigerCacheSizeGB \$\{SRC_WT_CACHE_GB\}" "\$MONGOD7_CPU_FEATURES"/.test(migrationControl),
|
||||
'the probe passes the feature spec so a modern MongoDB can be READ for migration without AVX');
|
||||
assert.ok(migrationControl.includes('bash "$SNAP/bin/cpu-exec" --features "${4:-}"'),
|
||||
'start_mongod routes every source mongod through cpu-exec');
|
||||
});
|
||||
|
||||
// --- #6466: per-item errors are not fatal; 3.x failures keep progress ---------
|
||||
|
||||
test('mongo3 importer: per-item errors no longer fail the whole migration', () => {
|
||||
assert.ok(!importer3.includes('state.success = state.errors.length < 10;'),
|
||||
'the <10-errors success threshold must be gone');
|
||||
assert.ok(importer3.includes('if (state.success !== false) state.success = true;'),
|
||||
'a completed run succeeds unless a FATAL path already failed it');
|
||||
});
|
||||
|
||||
test('modern importer: per-item errors no longer fail the whole migration', () => {
|
||||
assert.ok(!importerModern.includes('state.success = totalErrors < 10;'),
|
||||
'the <10-errors success threshold must be gone');
|
||||
assert.ok(importerModern.includes('if (state.success !== false) state.success = true;'));
|
||||
});
|
||||
|
||||
test('negative: fatal paths still fail — the success override respects them', () => {
|
||||
// Both importers must still contain fatal paths that set success = false
|
||||
// BEFORE the completion code (disk-full aborts, unusable tools).
|
||||
assert.ok(/state\.abort = true; state\.success = false/.test(importer3));
|
||||
assert.ok(/state\.abort = true; state\.success = false/.test(importerModern));
|
||||
});
|
||||
|
||||
test('a failed 3.x-source migration keeps the copied data and resumes', () => {
|
||||
assert.ok(migrationControl.includes('fail_3x_keep_progress() {'));
|
||||
// ... and the 3.x branch calls it (the modern branch may keep the MongoDB
|
||||
// fallback — mongod 7 CAN serve that data).
|
||||
const after3x = migrationControl.slice(migrationControl.indexOf('Detected MongoDB 3.x'));
|
||||
assert.ok(after3x.includes('fail_3x_keep_progress "$rc"'));
|
||||
assert.ok(!after3x.includes('\n fail_and_run_mongodb\n'),
|
||||
'3.x failures must NOT fall back to a mongod 7 that cannot open 3.x data files');
|
||||
});
|
||||
|
||||
test('fail_3x_keep_progress never discards the partial FerretDB or checkpoint', () => {
|
||||
const fn = migrationControl.match(/fail_3x_keep_progress\(\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(fn, 'function found');
|
||||
assert.ok(!fn[0].includes('discard_partial_ferretdb'));
|
||||
assert.ok(!fn[0].includes('migrate=off'), 'auto-migration must stay ON so the next start resumes');
|
||||
});
|
||||
|
||||
test('a successful migration clears the mongod start-failure counter', () => {
|
||||
const finish = migrationControl.match(/finish_success\(\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(finish, 'finish_success found');
|
||||
assert.ok(finish[0].includes('.mongod-start-failures'));
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,145 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node UI regression guard (no Meteor, no browser) for issue #6465:
|
||||
// desktop density regressions from the v7.98-v8.18 mobile UI work, restored to
|
||||
// the WeKan 6.09 look. Follows the repo's CSS-guard test style (see
|
||||
// tests/minicardLabelHeight.test.cjs).
|
||||
// Run: node tests/uiDensity.test.cjs
|
||||
//
|
||||
// Guards, with the regression each one blocks:
|
||||
// - base font 14px/18px (v7.98 clamp -> 18px on desktop, "+28.5% on everything")
|
||||
// - headings 22/18/16px (vw clamps -> 24/20/18px on desktop)
|
||||
// - All Boards wrapper 10px/15px (2vh/2vw -> huge empty header band)
|
||||
// - card details docks RIGHT (v8.18 floating window opened ON TOP of its own card)
|
||||
// - card canvas padding 20px (2.5vw -> ~48px white borders on FullHD)
|
||||
// - no force-all-card-text rule (everything in card details rendered title-sized)
|
||||
// - admin table headers wrap (nowrap + "Select all / Unselect all" text -> huge columns)
|
||||
// - zoom pill fixed-px (vh/vw pill taller than its 28px overflow-hidden row = cut off)
|
||||
// - one-click board settings cog (board menu needed two clicks)
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
const squish = s => s.replace(/\s+/g, ' ');
|
||||
|
||||
const layouts = read('client/components/main/layouts.css');
|
||||
const cardDetails = read('client/components/cards/cardDetails.css');
|
||||
const header = read('client/components/main/header.css');
|
||||
const settingBody = read('client/components/settings/settingBody.css');
|
||||
const peopleBody = read('client/components/settings/peopleBody.jade');
|
||||
const boardHeader = read('client/components/boards/boardHeader.jade');
|
||||
const listHeader = read('client/components/lists/listHeader.jade');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- Global density -----------------------------------------------------------
|
||||
|
||||
test('base font is the 6.09 fixed 14px/18px', () => {
|
||||
const base = layouts.match(/html,\s*\nbody,\s*\ninput,[\s\S]*?\{[\s\S]*?\}/);
|
||||
assert.ok(base, 'base font rule found');
|
||||
assert.ok(base[0].includes('font: 14px Roboto'));
|
||||
assert.ok(base[0].includes('line-height: 18px'));
|
||||
// negative: the desktop-inflating clamp must not come back
|
||||
assert.ok(!/font:\s*clamp\(/.test(base[0]));
|
||||
});
|
||||
|
||||
test('headings are the 6.09 sizes 22/18/16px', () => {
|
||||
assert.ok(/h1 \{\s*\n\s*font-size: 22px;/.test(layouts));
|
||||
assert.ok(/h2 \{\s*\n\s*font-size: 18px;/.test(layouts));
|
||||
assert.ok(/h3,\s*\nh4,\s*\nh5,\s*\nh6 \{\s*\n\s*font-size: 16px;/.test(layouts));
|
||||
});
|
||||
|
||||
test('All Boards wrapper is back to compact 10px/15px', () => {
|
||||
const wrapper = layouts.match(/#content > \.wrapper \{[\s\S]*?\}/);
|
||||
assert.ok(wrapper, 'wrapper rule found');
|
||||
assert.ok(wrapper[0].includes('margin-top: 10px'));
|
||||
assert.ok(wrapper[0].includes('padding: 15px'));
|
||||
// strip comments — the explanatory comment cites the old vh/vw values
|
||||
const declarationsOnly = wrapper[0].replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
assert.ok(!/[0-9]v[hw]/.test(squish(declarationsOnly)), 'no viewport-relative padding');
|
||||
});
|
||||
|
||||
// --- Card details window --------------------------------------------------------
|
||||
|
||||
test('card details default position docks to the END edge, not over the card', () => {
|
||||
const rule = cardDetails.match(
|
||||
/body\.desktop-mode \.card-details:not\(\.card-details-popup\):not\(\[style\*="left"\]\):not\(\[style\*="top"\]\) \{[\s\S]*?\}/,
|
||||
);
|
||||
assert.ok(rule, 'default-position rule found');
|
||||
assert.ok(rule[0].includes('inset-inline-start: auto'));
|
||||
assert.ok(rule[0].includes('inset-inline-end: 8px'));
|
||||
assert.ok(rule[0].includes('width: min(650px, 90vw)'));
|
||||
// negative: the v8.18 whole-board overlay anchored at the start edge
|
||||
assert.ok(!rule[0].includes('inset-inline-start: 20px'));
|
||||
});
|
||||
|
||||
test('card details canvas padding is the 6.09 fixed 20px', () => {
|
||||
assert.ok(/\.card-details \.card-details-canvas \{[\s\S]*?padding: 0 20px;/.test(cardDetails));
|
||||
assert.ok(!cardDetails.includes('padding: 0 2.5vw'), 'no ~48px viewport borders');
|
||||
});
|
||||
|
||||
test('the force-all-card-text-to-title-size block is gone', () => {
|
||||
assert.ok(!cardDetails.includes('.card-details select,'),
|
||||
'the v8.18 selector list that flattened the text hierarchy must stay removed');
|
||||
assert.ok(!cardDetails.includes('.card-details p,'));
|
||||
});
|
||||
|
||||
test('desktop card window uses a small 3px radius', () => {
|
||||
const fixed = cardDetails.match(
|
||||
/body\.desktop-mode \.card-details:not\(\.card-details-popup\) \{[\s\S]*?\}/,
|
||||
);
|
||||
assert.ok(fixed, 'desktop-mode rule found');
|
||||
assert.ok(fixed[0].includes('border-radius: 3px'));
|
||||
assert.ok(!fixed[0].includes('border-radius: 8px'));
|
||||
});
|
||||
|
||||
// --- Admin panel tables ---------------------------------------------------------
|
||||
|
||||
test('org/team feature column headers use compact icon links', () => {
|
||||
assert.ok(/a\.js-org-feature-all\(href="#", data-feature="orgSharedTemplates", data-value="true", title="\{\{_ 'select-all'\}\}"\)\s*\n\s*i\.fa\.fa-check-square-o/.test(peopleBody));
|
||||
assert.ok(/a\.js-team-feature-all\(href="#", data-feature="teamSyncMembersFromAuth", data-value="false", title="\{\{_ 'unselect-all'\}\}"\)\s*\n\s*i\.fa\.fa-square-o/.test(peopleBody));
|
||||
// negative: no full-text links blowing the column width up
|
||||
assert.ok(!/data-value="true"\) \{\{_ 'select-all'\}\}/.test(peopleBody));
|
||||
assert.ok(!/data-value="false"\) \{\{_ 'unselect-all'\}\}/.test(peopleBody));
|
||||
});
|
||||
|
||||
test('admin table headers may wrap (td keeps nowrap for scrolling)', () => {
|
||||
const th = settingBody.match(/\.setting-content \.content-body \.main-body table th \{[\s\S]*?\}/);
|
||||
assert.ok(th, 'th rule found');
|
||||
assert.ok(th[0].includes('white-space: normal'));
|
||||
const td = settingBody.match(/\.setting-content \.content-body \.main-body table td \{[\s\S]*?\}/);
|
||||
assert.ok(td, 'td rule found');
|
||||
assert.ok(td[0].includes('white-space: nowrap'));
|
||||
});
|
||||
|
||||
// --- Zoom pill -------------------------------------------------------------------
|
||||
|
||||
test('zoom pill fits inside the 28px quick-access row', () => {
|
||||
const pill = header.match(/#header-quick-access \.zoom-controls \{[\s\S]*?\}/);
|
||||
assert.ok(pill, 'zoom-controls rule found');
|
||||
assert.ok(pill[0].includes('padding: 2px 8px'));
|
||||
assert.ok(pill[0].includes('max-height: 24px'));
|
||||
// negative: no viewport-relative sizing that outgrew the clipped row
|
||||
assert.ok(!pill[0].includes('0.5vh 1vw'));
|
||||
});
|
||||
|
||||
// --- One-click board settings + list link -----------------------------------------
|
||||
|
||||
test('board header has a one-click board settings cog', () => {
|
||||
assert.ok(/a\.board-header-btn\.js-open-board-menu\(title="\{\{_ 'boardMenuPopup-title'\}\}"\)\s*\n\s*i\.fa\.fa-cog/.test(boardHeader));
|
||||
});
|
||||
|
||||
test('list More popup link input uses a real helper (#6459 companion)', () => {
|
||||
assert.ok(listHeader.includes('value="{{ absoluteUrl }}"'));
|
||||
assert.ok(!/value="\{\{ rootUrl \}\}"/.test(listHeader), 'rootUrl helper does not exist');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
Reference in new issue
Block a user