The KOReader plugin's incremental books pull went permanently stale: after a while it stopped receiving any updates made from other devices, and only deleting readest_library.sqlite3 + "Pull books now" recovered it (until it re-broke). The iOS/web library was unaffected. Root cause: since #4678 the server keys the books pull on the server-stamped synced_at column, and the web client (computeMaxTimestamp) advances its cursor from synced_at. The koplugin was left on updated_at: pullBooks advanced last_books_pulled_at from max(updated_at, deleted_at). updated_at is client-supplied, and the koplugin bumps it from the device clock (touchBook = os.time()*1000). An e-reader clock ahead of the server (or any row anywhere carrying a future updated_at) drove the cursor past server-now, so the server's synced_at > since filter returned nothing forever. The cursor was also shared between the pull side (compared vs server synced_at) and push-delta detection (getChangedBooks vs local updated_at), so it could not simply be retargeted. Fix: - parseSyncRow reads synced_at; new row_pull_cursor() prefers it and falls back to max(updated_at, deleted_at) for a pre-synced_at server, mirroring computeMaxTimestamp. - Split the cursor: last_books_pulled_at now tracks server synced_at (pull only); new last_books_pushed_at tracks local updated_at for getChangedBooks and is advanced on both pull and push, preserving push dedup. - v2 -> v3 migration seeds the push watermark from the old shared value and resets the pull cursor to 0, auto-healing already-stale installs with one full re-pull (no manual sqlite deletion) and no re-push storm. Co-authored-by: Claude Opus 4.8 (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.