From 310cc981773f5bb416a8995a9db723f9fb20ce94 Mon Sep 17 00:00:00 2001 From: aquelejota <250007329+aquelejota@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:31:45 -0300 Subject: [PATCH] fix: make session shard export deterministic exportedAt used Date.now(), so the shard payload changed on every export even when the session itself did not. git saw every shard as modified and autoPushOnIdle produced a commit touching every session file on each idle. Derive exportedAt from the session's own time_updated instead, so a repeat export of an unchanged session is byte-identical and pushes stay incremental. The existing regression test never caught this: under vitest the dynamic import("node:sqlite") fails to resolve, sqliteAvailable() reported false and every session test returned early. Fall back to createRequire so the builtin loads under test runners and the session tests actually execute. Refs #3 --- src/core/sessions.ts | 5 ++++- src/core/sqlite.ts | 23 +++++++++++++++++++++-- tests/sessions.test.ts | 7 ++++--- 3 files changed, 29 insertions(+), 6 deletions(-) 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); }); });