diff --git a/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts b/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts index fd3488b9..cfc903a3 100644 --- a/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts +++ b/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts @@ -116,8 +116,10 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter { return { path: path.relative(this.basePath, filePath).replace(/\\/g, "/") as FilePath, stat: { - ctime: stats?.ctimeMs ?? Date.now(), - mtime: stats?.mtimeMs ?? Date.now(), + // Floor to integer milliseconds; Linux fs.Stats.*Ms carry sub-millisecond + // precision, and timestamps are stored as integer ms everywhere else. + ctime: Math.floor(stats?.ctimeMs ?? Date.now()), + mtime: Math.floor(stats?.mtimeMs ?? Date.now()), size: stats?.size ?? 0, type: "file", }, diff --git a/src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts b/src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts index cd3e4156..602bc217 100644 --- a/src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts +++ b/src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts @@ -84,6 +84,27 @@ describe("CLIStorageEventManagerAdapter", () => { expect(created.stat?.size).toBe(42); }); + it("floors sub-millisecond stat timestamps so mobile clients do not receive floats", async () => { + const basePath = "/vault/base"; + const adapter = new CLIStorageEventManagerAdapter(basePath, undefined, true); + const handlers = makeHandlers(); + + await adapter.watch.beginWatch(handlers); + + const addCallback = mockWatcher.on.mock.calls.find(([event]) => event === "add")![1] as ( + filePath: string, + stats: any + ) => void; + + // Linux fs.Stats carry nanosecond-derived sub-millisecond precision. + const floatStats = { ctimeMs: 1778511180024.462, mtimeMs: 1778511180999.913, size: 7 }; + addCallback(`${basePath}/note.md`, floatStats); + + const created = (handlers.onCreate as ReturnType).mock.calls[0][0] as NodeFile; + expect(created.stat?.ctime).toBe(1778511180024); + expect(created.stat?.mtime).toBe(1778511180999); + }); + it("close() calls watcher.close()", async () => { const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true); const handlers = makeHandlers();