diff --git a/src/core/sessions.ts b/src/core/sessions.ts index d59170a..07dcaa2 100644 --- a/src/core/sessions.ts +++ b/src/core/sessions.ts @@ -165,7 +165,10 @@ function buildShard(db: SqliteDatabase, sessionId: string, tables: string[]): Se formatVersion: 1, session: sessionRow, tables: {}, - exportedAt: Date.now(), + // Derived from the session itself, never wall-clock time. A repeat export + // of an unchanged session must produce identical bytes, otherwise every + // push rewrites every shard and import/pull churn is unavoidable. + exportedAt: Number(sessionRow.time_updated ?? 0), }; const projectId = sessionRow.project_id; diff --git a/src/core/sqlite.ts b/src/core/sqlite.ts index a87d415..1e9aad7 100644 --- a/src/core/sqlite.ts +++ b/src/core/sqlite.ts @@ -14,6 +14,8 @@ * disabled with a clear message rather than failing halfway through. */ +import { createRequire } from "node:module"; + export interface SqliteStatement { all(...params: unknown[]): any[]; run(...params: unknown[]): unknown; @@ -39,6 +41,23 @@ async function importBunSqlite(): Promise { return import(/* @vite-ignore */ /* webpackIgnore: true */ BUN_SQLITE); } +/** + * Load the Node built-in SQLite driver. + * + * The plain dynamic import is what works under Node and Bun. Test runners such + * as vitest intercept dynamic imports and try to resolve `node:sqlite` as a + * project file, which makes the driver undetectable and silently turns every + * session test into an early return. `createRequire` bypasses the bundler and + * loads the real builtin, so those tests actually run. + */ +async function importNodeSqlite(): Promise { + try { + return await import("node:sqlite"); + } catch { + return createRequire(import.meta.url)("node:sqlite"); + } +} + let cachedDriver: "bun" | "node" | "none" | undefined; async function detectDriver(): Promise<"bun" | "node" | "none"> { @@ -53,7 +72,7 @@ async function detectDriver(): Promise<"bun" | "node" | "none"> { } } try { - const mod: any = await import("node:sqlite"); + const mod: any = await importNodeSqlite(); if (mod?.DatabaseSync) { cachedDriver = "node"; return cachedDriver; @@ -102,7 +121,7 @@ export async function openDatabase( } if (driver === "node") { - const { DatabaseSync } = (await import("node:sqlite")) as any; + const { DatabaseSync } = (await importNodeSqlite()) as any; const db = new DatabaseSync(file, options.readOnly ? { readOnly: true } : {}); return { prepare: (sql: string) => { diff --git a/tests/sessions.test.ts b/tests/sessions.test.ts index 8f7c73c..aadfee9 100644 --- a/tests/sessions.test.ts +++ b/tests/sessions.test.ts @@ -305,13 +305,14 @@ describe("export and import", () => { await exportSessions(source, repo, { ...settings(), now }, silentReporter); const shard = path.join(repo, "_sessions", "ses_recent.json.gz"); - const before = fs.statSync(shard).mtimeMs; + const before = fs.readFileSync(shard); await new Promise((resolve) => setTimeout(resolve, 20)); await exportSessions(source, repo, { ...settings(), now }, silentReporter); - // An unchanged session must not produce a new diff on every push. - expect(fs.statSync(shard).mtimeMs).toBe(before); + // An unchanged session must not produce a new diff on every push — same + // bytes, same mtime, no diff for git to pick up. + expect(fs.readFileSync(shard).equals(before)).toBe(true); }); });