Files
readest/apps/readest.koplugin/spec
Huang Xin 0b180da6a6 fix(koplugin): key library pull cursor on synced_at to stop stale library (#4934) (#4944)
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>
2026-07-05 17:52:01 +02:00
..

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 wrapping lsqlite3complete. 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/err callable).
  • require("datastorage") → fake DataStorage:getSettingsDir() returning a per-test mktemp -d path.
  • require("device") → stub Device.canUseWAL() == true, Device.screen with getWidth/getHeight.
  • G_reader_settings (global) → in-memory readSetting/saveSetting/flush.

Each spec calls require("spec_helper").reset() in before_each to wipe state.

Adding a new module

  1. Write production code at apps/readest.koplugin/library/foo.lua.
  2. Write apps/readest.koplugin/spec/library/foo_spec.lua.
  3. Run pnpm test:lua from the repo root.
  4. Run pnpm lint:lua to 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.