fix(plugin-kit): the SQLite reader never opened anything on Linux #78

Merged
enricobuehler merged 1 commits from worktree-plugin-kit-sqlite-uri-fix into main 2026-08-06 18:24:09 +00:00
Owner

Found by running the lutris plugin's own release gate against a live host on .21 — the box you just installed Lutris on. parity --compare reported 1 missing: the plugin produced no entry for the one game Lutris had, while the host's built-in scanner produced it fine. scan agreed — present: 0 games. detect still said present, because detect only stats the file.

Bug 1 — openReadOnly could never open a database on Linux

openReadOnly builds a file:<path>?immutable=1 URI, which is the right idea: immutable=1 is what makes this a pure read that cannot lock a running launcher's database or spawn WAL sidecars beside it. But it opened that name with { readonly: true }, and the options object does not enable SQLite's URI filename parsing. Without SQLITE_OPEN_URI the name is taken literally, no such file exists, and the open throws:

SQLiteError: unable to open database file

Every path then degrades that to silence by design: openReadOnly returns undefined for "this launcher isn't installed", withReadOnlyDb passes it through, and callers write withReadOnlyDb(...) ?? []. So a total, permanent inability to read any database was indistinguishable from an empty library. The lutris plugin reported 0 games on every Linux box, always.

It is platform-split, which is why it survived review and local runs: macOS links Apple's system SQLite, built with URI filenames enabled, so the identical call succeeds there. Linux uses Bun's bundled SQLite, which is not. Measured both ways on bun 1.3.14.

Fixed by passing the flags explicitly — SQLITE_OPEN_READONLY | SQLITE_OPEN_URI. ({ readonly: true, uri: true } is not a supported option shape; also measured.)

Bug 2 — the release gate could not fail

parity --compare sets process.exitCode = 1 on a mismatch and then returns normally — a red parity is a finished comparison, not a crashed command. runPluginCli then assigned process.exitCode = 0 unconditionally after the effect resolved, overwriting it.

So the one verb both plugin READMEs document as the release gate — "exits non-zero on any difference", "do not publish a version whose parity run is red" — always exited 0, and any scripted use of it passed. Measured against the live host: parity FAILED — 1 missing, exit 0.

Now process.exitCode ??= 0, so a code a command set deliberately survives.

Tests

The sqlite helper had no coverage at all — which is precisely why a total failure shipped looking like an empty library. Added five cases against a real database file: reads rows back, handles a path needing URI escaping, withReadOnlyDb round-trips, an absent file is undefined not a throw, and a bad query still degrades to [].

Verified they actually catch it rather than assuming:

shipped code with fix
library-parsers.test.ts on Linux 4 of 5 new tests fail 21/21 pass
full suite 61/61 pass
typecheck / build clean

⚠ On macOS the new tests pass either way, because the platform accepts the URI. Linux is where these plugins run and where CI runs, so CI will hold the line.

End to end on .21

With the fix applied to the deployed kit:

  • lutris scan: 0 → 1 games
  • parity --compare: parity OK — 1 entries identical (+1 expected launcher entry)
  • a deliberately doctored baseline now exits 1 instead of 0

Version

0.3.0 → 0.3.1. Publishing is a plugin-kit-v0.3.1 tag.

The plugins will not pick this up automatically. lutris and heroic depend on ^0.3.0, but their bun.lock files pin 0.3.0 exactly, so each needs a lockfile refresh after the kit publishes before its parity run means anything. That is the remaining chain before lutris/heroic can be tagged.

Found by running the lutris plugin's own release gate against a live host on `.21` — the box you just installed Lutris on. `parity --compare` reported **`1 missing`**: the plugin produced no entry for the one game Lutris had, while the host's built-in scanner produced it fine. `scan` agreed — `present: 0 games`. `detect` still said `present`, because detect only stats the file. ## Bug 1 — `openReadOnly` could never open a database on Linux `openReadOnly` builds a `file:<path>?immutable=1` URI, which is the right idea: `immutable=1` is what makes this a pure read that cannot lock a running launcher's database or spawn WAL sidecars beside it. But it opened that name with `{ readonly: true }`, and **the options object does not enable SQLite's URI filename parsing**. Without `SQLITE_OPEN_URI` the name is taken literally, no such file exists, and the open throws: ``` SQLiteError: unable to open database file ``` Every path then degrades that to silence *by design*: `openReadOnly` returns `undefined` for "this launcher isn't installed", `withReadOnlyDb` passes it through, and callers write `withReadOnlyDb(...) ?? []`. So a total, permanent inability to read **any** database was indistinguishable from an empty library. **The lutris plugin reported 0 games on every Linux box, always.** It is platform-split, which is why it survived review and local runs: macOS links Apple's system SQLite, built with URI filenames enabled, so the identical call succeeds there. Linux uses Bun's bundled SQLite, which is not. Measured both ways on bun 1.3.14. Fixed by passing the flags explicitly — `SQLITE_OPEN_READONLY | SQLITE_OPEN_URI`. (`{ readonly: true, uri: true }` is not a supported option shape; also measured.) ## Bug 2 — the release gate could not fail `parity --compare` sets `process.exitCode = 1` on a mismatch and then **returns normally** — a red parity is a finished comparison, not a crashed command. `runPluginCli` then assigned `process.exitCode = 0` unconditionally after the effect resolved, overwriting it. So the one verb both plugin READMEs document as *the* release gate — "exits non-zero on any difference", "**do not publish a version whose parity run is red**" — always exited 0, and any scripted use of it passed. Measured against the live host: `parity FAILED — 1 missing`, exit `0`. Now `process.exitCode ??= 0`, so a code a command set deliberately survives. ## Tests The sqlite helper had **no coverage at all** — which is precisely why a total failure shipped looking like an empty library. Added five cases against a *real* database file: reads rows back, handles a path needing URI escaping, `withReadOnlyDb` round-trips, an absent file is `undefined` not a throw, and a bad query still degrades to `[]`. Verified they actually catch it rather than assuming: | | shipped code | with fix | |---|---|---| | `library-parsers.test.ts` on **Linux** | **4 of 5 new tests fail** | 21/21 pass | | full suite | — | **61/61 pass** | | typecheck / build | — | clean | ⚠ On macOS the new tests pass either way, because the platform accepts the URI. Linux is where these plugins run and where CI runs, so CI will hold the line. ## End to end on `.21` With the fix applied to the deployed kit: - lutris `scan`: **0 → 1 games** - `parity --compare`: **`parity OK — 1 entries identical`** (+1 expected launcher entry) - a deliberately doctored baseline now exits **1** instead of 0 ## Version `0.3.0 → 0.3.1`. Publishing is a `plugin-kit-v0.3.1` tag. ⚠ **The plugins will not pick this up automatically.** lutris and heroic depend on `^0.3.0`, but their `bun.lock` files pin `0.3.0` exactly, so each needs a lockfile refresh after the kit publishes before its parity run means anything. That is the remaining chain before lutris/heroic can be tagged.
enricobuehler added 1 commit 2026-08-06 18:14:32 +00:00
fix(plugin-kit): the SQLite reader never opened anything on Linux
nix / flake (pull_request) Failing after 1s
ci / bun-nix (pull_request) Successful in 22s
ci / web (pull_request) Successful in 58s
ci / docs-site (pull_request) Successful in 1m35s
ci / rust-arm64 (pull_request) Successful in 2m2s
ci / rust (pull_request) Successful in 6m15s
4bcb3794c5
Found while running the lutris plugin's own release gate against a live host on
.21: `parity --compare` reported `1 missing` — the plugin produced NO entry for
the one game Lutris had, while the host's built-in scanner produced it fine.
`scan` agreed: "present: 0 games". `detect` still said "present", because detect
only stats the file.

`openReadOnly` builds a `file:<path>?immutable=1` URI — the right idea, since
`immutable=1` is what makes this a pure read that cannot lock a running
launcher's database or spawn WAL sidecars next to it. But it opened that name
with `{ readonly: true }`, and the options object does NOT enable SQLite's URI
filename parsing. Without SQLITE_OPEN_URI the name is taken literally, no such
file exists, and the open throws:

    SQLiteError: unable to open database file

Every path here then degrades that to silence by design: `openReadOnly` returns
`undefined` for "this launcher isn't installed", `withReadOnlyDb` passes the
`undefined` through, and callers write `withReadOnlyDb(...) ?? []`. So a total,
permanent failure to read ANY database was indistinguishable from an empty
library. The lutris plugin reported 0 games on every Linux box, always.

It is platform-split, which is why it survived review and local runs: macOS
links Apple's system SQLite, which is built with URI filenames enabled, so the
same call succeeds there. Linux uses Bun's bundled SQLite, which is not. MEASURED
both ways on bun 1.3.14.

Fixed by passing the flags explicitly — SQLITE_OPEN_READONLY | SQLITE_OPEN_URI.
(`{ readonly: true, uri: true }` is not a supported option shape; also measured.)

And the second half, which is why nothing caught it: `parity --compare` sets
`process.exitCode = 1` on a mismatch and then returns NORMALLY — a red parity is
a finished comparison, not a crashed command. `runPluginCli` then assigned
`process.exitCode = 0` unconditionally after the effect resolved, overwriting it.
So the one verb both plugin READMEs document as the release gate — "exits
non-zero on any difference", "do not publish a version whose parity run is red" —
always exited 0, and any scripted use of it passed. Now `??= 0`, so a code a
command set deliberately survives.

Tests: the sqlite helper had NO coverage at all, which is the whole reason a
total failure shipped looking like an empty library. Added five cases against a
REAL database file — reads rows back, handles a path needing URI escaping,
withReadOnlyDb round-trips, an absent file is `undefined` not a throw, and a bad
query still degrades to []. Verified they actually catch it: against the shipped
code on Linux, 4 of the 5 fail; with the fix, 21/21 in that file and 61/61 across
the suite, typecheck and build clean.

End to end on .21 with the fix: lutris `scan` goes 0 -> 1 games and
`parity --compare` reports "parity OK — 1 entries identical". A deliberately
doctored baseline now exits 1 instead of 0.

0.3.0 -> 0.3.1.
enricobuehler merged commit 442558e503 into main 2026-08-06 18:24:09 +00:00
enricobuehler deleted branch worktree-plugin-kit-sqlite-uri-fix 2026-08-06 18:24:14 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#78