@tikoci/rosetta 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/rosetta",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "RouterOS documentation as SQLite FTS5 — RAG search + command glossary via MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/mcp.ts CHANGED
@@ -66,10 +66,13 @@ async function ensureDbReady(log: (msg: string) => void): Promise<void> {
66
66
  const dbPath = resolveDbPath(import.meta.dirname);
67
67
  const runningVersion = resolveVersion(import.meta.dirname);
68
68
 
69
- /** Probe an existing DB. Returns null if the file is missing or unreadable. */
69
+ /** Probe an existing DB. Returns null if the file is missing or unreadable.
70
+ * Do NOT pass { readonly: true } — freshly written SQLite WAL-mode files fail
71
+ * to open readonly on macOS until a read-write connection initialises the WAL
72
+ * shared-memory file. Same gotcha as setup.ts::probeDb. */
70
73
  function probe(): { pages: number; schemaVersion: number; releaseTag: string | null } | null {
71
74
  try {
72
- const check = new sqlite(dbPath, { readonly: true });
75
+ const check = new sqlite(dbPath);
73
76
  const pages = (check.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number }).c;
74
77
  const ver = (check.prepare("PRAGMA user_version").get() as { user_version: number }).user_version;
75
78
  let releaseTag: string | null = null;
@@ -202,6 +202,22 @@ describe("setup.ts", () => {
202
202
  expect(src).toContain("Still incompatible after re-download");
203
203
  expect(src).toMatch(/Still incompatible[\s\S]{0,400}process\.exit\(1\)/);
204
204
  });
205
+
206
+ test("no DB open uses { readonly: true } (WAL-shm init trap on macOS)", () => {
207
+ // Freshly-renamed WAL-mode DBs fail to open readonly on macOS until a
208
+ // read-write connection initialises the .shm file. downloadDb explicitly
209
+ // deletes .wal/.shm before the rename, so every subsequent open must be
210
+ // read-write. Regressing this ships a bunx path that can't open its own
211
+ // validated download (see v0.8.0 "DB=unreadable" bug).
212
+ for (const file of ["src/mcp.ts", "src/setup.ts", "src/db.ts"]) {
213
+ // Strip line + block comments before scanning, so the "do NOT pass
214
+ // { readonly: true }" warnings themselves don't trip the check.
215
+ const src = readText(file)
216
+ .replace(/\/\*[\s\S]*?\*\//g, "")
217
+ .replace(/\/\/.*$/gm, "");
218
+ expect(src).not.toMatch(/readonly\s*:\s*true/);
219
+ }
220
+ });
205
221
  });
206
222
 
207
223
  // ---------------------------------------------------------------------------
package/src/setup.test.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import sqlite from "bun:sqlite";
10
10
  import { afterAll, describe, expect, test } from "bun:test";
11
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import path from "node:path";
14
14
 
@@ -91,6 +91,36 @@ describe("probeDb", () => {
91
91
  expect(probe?.releaseTag).toBe("v0.0.0-test");
92
92
  });
93
93
 
94
+ test("opens a freshly-renamed WAL-mode DB with no .shm sibling", () => {
95
+ // Reproduces the exact state downloadDb leaves the DB in: journal_mode=WAL
96
+ // on disk, but the .wal/.shm siblings are deleted just before the rename.
97
+ // On macOS, opening such a file with { readonly: true } fails with
98
+ // "unable to open database file" because SQLite cannot create the shm
99
+ // from a read-only handle. probeDb intentionally opens read-write.
100
+ const dbFile = path.join(tmp, "wal-no-shm.db");
101
+ const seed = new sqlite(dbFile);
102
+ seed.exec("PRAGMA journal_mode = WAL");
103
+ seed.run(`PRAGMA user_version = ${SCHEMA_VERSION};`);
104
+ seed.run("CREATE TABLE pages (id INTEGER PRIMARY KEY);");
105
+ seed.run("CREATE TABLE commands (id INTEGER PRIMARY KEY);");
106
+ seed.run("INSERT INTO pages (id) VALUES (1), (2), (3);");
107
+ seed.run("INSERT INTO commands (id) VALUES (1), (2);");
108
+ seed.close();
109
+
110
+ // Simulate downloadDb's post-rename cleanup — WAL file on disk marks WAL
111
+ // mode in the header, but the transient .wal/.shm are gone.
112
+ for (const suffix of ["-wal", "-shm"]) {
113
+ const p = dbFile + suffix;
114
+ if (existsSync(p)) unlinkSync(p);
115
+ }
116
+
117
+ const probe = probeDb(dbFile);
118
+ expect(probe).not.toBeNull();
119
+ expect(probe?.schemaVersion).toBe(SCHEMA_VERSION);
120
+ expect(probe?.pages).toBe(3);
121
+ expect(probe?.commands).toBe(2);
122
+ });
123
+
94
124
  test("releaseTag is null when db_meta is absent (pre-v5 schema)", () => {
95
125
  const dbFile = path.join(tmp, "no-meta.db");
96
126
  const db = new sqlite(dbFile);
package/src/setup.ts CHANGED
@@ -26,12 +26,14 @@ const MIN_COMMANDS = 1000;
26
26
  const MIN_DECOMPRESSED_BYTES = 50 * 1024 * 1024; // 50 MB
27
27
  const SQLITE_MAGIC = "SQLite format 3\0";
28
28
 
29
- /** Check if a DB file exists and has actual page data */
29
+ /** Check if a DB file exists and has actual page data.
30
+ * Opens read-write — see probeDb's note: freshly written WAL-mode files can
31
+ * fail to open readonly on macOS when the .shm file is missing. */
30
32
  function dbHasData(dbPath: string): boolean {
31
33
  if (!existsSync(dbPath)) return false;
32
34
  try {
33
35
  const { default: sqlite } = require("bun:sqlite");
34
- const check = new sqlite(dbPath, { readonly: true });
36
+ const check = new sqlite(dbPath);
35
37
  const row = check.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number };
36
38
  check.close();
37
39
  return row.c > 0;