Pull skipped notes carrying a deleted_at tombstone but never removed the matching local annotation. A highlight deleted on Readest therefore lingered in KOReader, and a later push (notably a full sync) re-uploaded it, resurrecting the note on the server and making it reappear on every device. Add removeDeletedAnnotations, invoked at the start of the pull callback, to drop local annotations the server has tombstoned. Tombstones are matched by stored id, by the hash-derived id for native KOReader highlights, or by position/page xpointer. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
readest.koplugin tests
Unit tests for apps/readest.koplugin/library/ modules. Runs under LuaJIT
2.1 (the runtime KOReader uses) via busted.
Toolchain
One-time per machine:
# macOS
brew install luajit luarocks
# Linux (Debian/Ubuntu)
sudo apt-get install luajit luarocks
# Then, regardless of OS:
luarocks --lua-version=5.1 install busted
luarocks --lua-version=5.1 install lsqlite3complete
The --lua-version=5.1 flag is required: LuaJIT identifies itself as Lua 5.1, and we install rocks against that runtime so production code (which targets LuaJIT) and test code share a Lua interpreter.
Running
From the repo root:
pnpm test:lua
Or from this directory:
eval "$(luarocks --lua-version=5.1 path)"
busted --lua=$(which luajit)
Layout
spec/
├── spec_helper.lua # KOReader stubs + lua-ljsqlite3 shim (loaded once)
├── library/
│ ├── smoke_spec.lua # Sanity check that the harness boots
│ └── *_spec.lua # One per module under library/
└── README.md # This file
What spec_helper provides
require("lua-ljsqlite3/init")→ returns a SQLite shim wrappinglsqlite3complete. Exposes the subset of the lua-ljsqlite3 API our library modules use (open,exec,prepare,bind1,step,reset,clearbind,close, etc).require("logger")→ no-op logger (warn/info/dbg/errcallable).require("datastorage")→ fakeDataStorage:getSettingsDir()returning a per-testmktemp -dpath.require("device")→ stubDevice.canUseWAL() == true,Device.screenwithgetWidth/getHeight.G_reader_settings(global) → in-memoryreadSetting/saveSetting/flush.
Each spec calls require("spec_helper").reset() in before_each to wipe state.
Adding a new module
- Write production code at
apps/readest.koplugin/library/foo.lua. - Write
apps/readest.koplugin/spec/library/foo_spec.lua. - Run
pnpm test:luafrom the repo root. - Run
pnpm lint:luato syntax-check (LuaJIT bytecode compile).
Why LuaJIT and not stock Lua?
KOReader runs LuaJIT exclusively. LuaJIT extends Lua 5.1 with FFI and a few syntax tweaks; stock Lua 5.4 has features (integer division //, bit operators ~, <const> annotations) that LuaJIT rejects. Running tests under LuaJIT catches these incompatibilities at test time instead of when KOReader fails to load the plugin.