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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by running the lutris plugin's own release gate against a live host on
.21— the box you just installed Lutris on.parity --comparereported1 missing: the plugin produced no entry for the one game Lutris had, while the host's built-in scanner produced it fine.scanagreed —present: 0 games.detectstill saidpresent, because detect only stats the file.Bug 1 —
openReadOnlycould never open a database on LinuxopenReadOnlybuilds afile:<path>?immutable=1URI, which is the right idea:immutable=1is 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. WithoutSQLITE_OPEN_URIthe name is taken literally, no such file exists, and the open throws:Every path then degrades that to silence by design:
openReadOnlyreturnsundefinedfor "this launcher isn't installed",withReadOnlyDbpasses it through, and callers writewithReadOnlyDb(...) ?? []. 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 --comparesetsprocess.exitCode = 1on a mismatch and then returns normally — a red parity is a finished comparison, not a crashed command.runPluginClithen assignedprocess.exitCode = 0unconditionally 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, exit0.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,
withReadOnlyDbround-trips, an absent file isundefinednot a throw, and a bad query still degrades to[].Verified they actually catch it rather than assuming:
library-parsers.test.tson Linux⚠ 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
.21With the fix applied to the deployed kit:
scan: 0 → 1 gamesparity --compare:parity OK — 1 entries identical(+1 expected launcher entry)Version
0.3.0 → 0.3.1. Publishing is aplugin-kit-v0.3.1tag.⚠ The plugins will not pick this up automatically. lutris and heroic depend on
^0.3.0, but theirbun.lockfiles pin0.3.0exactly, 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 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.