feat(shared): add fsync-skip tracker and path-environment classifier
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
|
||||
import {
|
||||
classifyPathEnvironment,
|
||||
describePathClassification,
|
||||
} from "./classify-path-environment"
|
||||
|
||||
describe("classifyPathEnvironment", () => {
|
||||
it("classifies macOS iCloud path as icloud", () => {
|
||||
expect(
|
||||
classifyPathEnvironment(
|
||||
"/Users/x/Library/Mobile Documents/com~apple~CloudDocs/project/file.txt",
|
||||
),
|
||||
).toBe("icloud")
|
||||
})
|
||||
|
||||
it("classifies OneDrive path on unix style", () => {
|
||||
expect(classifyPathEnvironment("/Users/x/OneDrive/foo")).toBe("onedrive")
|
||||
})
|
||||
|
||||
it("classifies OneDrive path on windows style", () => {
|
||||
expect(classifyPathEnvironment("C:\\Users\\x\\OneDrive\\foo")).toBe("onedrive")
|
||||
})
|
||||
|
||||
it("classifies macOS Desktop path as desktop-sync", () => {
|
||||
expect(classifyPathEnvironment("/Users/x/Desktop/foo")).toBe("desktop-sync")
|
||||
})
|
||||
|
||||
it("classifies /Volumes path as network-drive", () => {
|
||||
expect(classifyPathEnvironment("/Volumes/NetworkShare/foo")).toBe("network-drive")
|
||||
})
|
||||
|
||||
it("classifies random path as unknown", () => {
|
||||
expect(classifyPathEnvironment("/tmp/foo")).toBe("unknown")
|
||||
})
|
||||
|
||||
it("classifies empty string as unknown", () => {
|
||||
expect(classifyPathEnvironment("")).toBe("unknown")
|
||||
})
|
||||
|
||||
it("matches OneDrive case-insensitively", () => {
|
||||
expect(classifyPathEnvironment("/Users/x/oNeDrIvE/foo")).toBe("onedrive")
|
||||
})
|
||||
})
|
||||
|
||||
describe("describePathClassification", () => {
|
||||
it("returns human-readable descriptions", () => {
|
||||
expect(describePathClassification("icloud")).toBe("iCloud Drive")
|
||||
expect(describePathClassification("onedrive")).toBe("OneDrive")
|
||||
expect(describePathClassification("desktop-sync")).toBe("Desktop sync (macOS)")
|
||||
expect(describePathClassification("network-drive")).toBe("Network drive")
|
||||
expect(describePathClassification("unknown")).toBe(
|
||||
"filesystem that does not support fsync",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { homedir } from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
export type PathClassification =
|
||||
| "icloud"
|
||||
| "onedrive"
|
||||
| "desktop-sync"
|
||||
| "network-drive"
|
||||
| "unknown"
|
||||
|
||||
function normalizeInputPath(absolutePath: string): string {
|
||||
return absolutePath.replaceAll("\\", "/")
|
||||
}
|
||||
|
||||
function isUnderPath(normalizedPath: string, normalizedParentPath: string): boolean {
|
||||
return normalizedPath === normalizedParentPath || normalizedPath.startsWith(`${normalizedParentPath}/`)
|
||||
}
|
||||
|
||||
export function classifyPathEnvironment(absolutePath: string): PathClassification {
|
||||
if (absolutePath.length === 0) return "unknown"
|
||||
|
||||
const normalizedPath = normalizeInputPath(absolutePath)
|
||||
const lowercasePath = normalizedPath.toLowerCase()
|
||||
if (lowercasePath.includes("/onedrive") || lowercasePath.includes("/onedrive/")) {
|
||||
return "onedrive"
|
||||
}
|
||||
|
||||
if (normalizedPath.includes("/Library/Mobile Documents/")) {
|
||||
return "icloud"
|
||||
}
|
||||
|
||||
if (isUnderPath(normalizedPath, "/Volumes")) {
|
||||
return "network-drive"
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath.startsWith("/Users/")
|
||||
&& (normalizedPath.includes("/Desktop/") || normalizedPath.endsWith("/Desktop")
|
||||
|| normalizedPath.includes("/Documents/") || normalizedPath.endsWith("/Documents"))
|
||||
) {
|
||||
return "desktop-sync"
|
||||
}
|
||||
|
||||
const normalizedHome = normalizeInputPath(homedir())
|
||||
const desktopPath = normalizeInputPath(path.join(normalizedHome, "Desktop"))
|
||||
const documentsPath = normalizeInputPath(path.join(normalizedHome, "Documents"))
|
||||
|
||||
if (isUnderPath(normalizedPath, desktopPath) || isUnderPath(normalizedPath, documentsPath)) {
|
||||
return "desktop-sync"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
export function describePathClassification(pathClassification: PathClassification): string {
|
||||
switch (pathClassification) {
|
||||
case "icloud":
|
||||
return "iCloud Drive"
|
||||
case "onedrive":
|
||||
return "OneDrive"
|
||||
case "desktop-sync":
|
||||
return "Desktop sync (macOS)"
|
||||
case "network-drive":
|
||||
return "Network drive"
|
||||
case "unknown":
|
||||
return "filesystem that does not support fsync"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test"
|
||||
|
||||
import {
|
||||
clearAllSkips,
|
||||
drainSkipsAfter,
|
||||
recordFsyncSkip,
|
||||
} from "./fsync-skip-tracker"
|
||||
|
||||
type PathClassification =
|
||||
| "icloud"
|
||||
| "onedrive"
|
||||
| "desktop-sync"
|
||||
| "network-drive"
|
||||
| "unknown"
|
||||
|
||||
function recordSkip(index: number, pathClassification: PathClassification = "unknown"): void {
|
||||
recordFsyncSkip({
|
||||
filePath: `/tmp/file-${index}.txt`,
|
||||
contextLabel: `atomicWrite:/tmp/file-${index}.txt`,
|
||||
errorCode: "EPERM",
|
||||
message: "operation not permitted",
|
||||
pathClassification,
|
||||
})
|
||||
}
|
||||
|
||||
describe("fsync-skip-tracker", () => {
|
||||
beforeEach(() => {
|
||||
clearAllSkips()
|
||||
})
|
||||
|
||||
it("recordFsyncSkip adds entry with timestamp", () => {
|
||||
const before = Date.now()
|
||||
recordSkip(1)
|
||||
const entries = drainSkipsAfter(0)
|
||||
|
||||
expect(entries).toHaveLength(1)
|
||||
expect(entries[0]?.filePath).toBe("/tmp/file-1.txt")
|
||||
expect(entries[0]?.timestamp).toBeGreaterThanOrEqual(before)
|
||||
})
|
||||
|
||||
it("drainSkipsAfter(timestamp) returns entries strictly after the timestamp", async () => {
|
||||
recordSkip(1)
|
||||
const firstTimestamp = Date.now()
|
||||
|
||||
await Bun.sleep(2)
|
||||
|
||||
recordSkip(2)
|
||||
const drained = drainSkipsAfter(firstTimestamp)
|
||||
expect(drained).toHaveLength(1)
|
||||
expect(drained[0]?.filePath).toBe("/tmp/file-2.txt")
|
||||
})
|
||||
|
||||
it("drainSkipsAfter removes drained entries from buffer", () => {
|
||||
recordSkip(1)
|
||||
recordSkip(2)
|
||||
|
||||
const drained = drainSkipsAfter(0)
|
||||
expect(drained).toHaveLength(2)
|
||||
expect(drainSkipsAfter(0)).toEqual([])
|
||||
})
|
||||
|
||||
it("buffer is bounded to max 200 entries and drops oldest on overflow", () => {
|
||||
for (let index = 1; index <= 205; index += 1) {
|
||||
recordSkip(index)
|
||||
}
|
||||
|
||||
const drained = drainSkipsAfter(0)
|
||||
expect(drained).toHaveLength(200)
|
||||
expect(drained[0]?.filePath).toBe("/tmp/file-6.txt")
|
||||
expect(drained[199]?.filePath).toBe("/tmp/file-205.txt")
|
||||
})
|
||||
|
||||
it("multiple records with same path are kept", () => {
|
||||
recordSkip(1)
|
||||
recordFsyncSkip({
|
||||
filePath: "/tmp/file-1.txt",
|
||||
contextLabel: "acquireLock:/tmp/file-1.txt",
|
||||
errorCode: "EPERM",
|
||||
message: "second",
|
||||
pathClassification: "unknown",
|
||||
})
|
||||
|
||||
const drained = drainSkipsAfter(0)
|
||||
expect(drained).toHaveLength(2)
|
||||
expect(drained[0]?.filePath).toBe("/tmp/file-1.txt")
|
||||
expect(drained[1]?.filePath).toBe("/tmp/file-1.txt")
|
||||
})
|
||||
|
||||
it("drainSkipsAfter(0) returns all entries", () => {
|
||||
recordSkip(1)
|
||||
recordSkip(2)
|
||||
|
||||
const drained = drainSkipsAfter(0)
|
||||
expect(drained).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("empty buffer returns empty array", () => {
|
||||
expect(drainSkipsAfter(0)).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PathClassification } from "./classify-path-environment"
|
||||
|
||||
export type FsyncSkipEntry = {
|
||||
filePath: string
|
||||
contextLabel: string
|
||||
errorCode: string
|
||||
message: string
|
||||
pathClassification: PathClassification
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const MAX_SKIPS = 200
|
||||
const fsyncSkips: FsyncSkipEntry[] = []
|
||||
|
||||
export function recordFsyncSkip(entry: Omit<FsyncSkipEntry, "timestamp">): void {
|
||||
fsyncSkips.push({ ...entry, timestamp: Date.now() })
|
||||
|
||||
if (fsyncSkips.length > MAX_SKIPS) {
|
||||
fsyncSkips.splice(0, fsyncSkips.length - MAX_SKIPS)
|
||||
}
|
||||
}
|
||||
|
||||
export function drainSkipsAfter(timestampMs: number): FsyncSkipEntry[] {
|
||||
const drainedEntries: FsyncSkipEntry[] = []
|
||||
const retainedEntries: FsyncSkipEntry[] = []
|
||||
|
||||
for (const entry of fsyncSkips) {
|
||||
if (entry.timestamp > timestampMs) {
|
||||
drainedEntries.push(entry)
|
||||
continue
|
||||
}
|
||||
|
||||
retainedEntries.push(entry)
|
||||
}
|
||||
|
||||
fsyncSkips.splice(0, fsyncSkips.length, ...retainedEntries)
|
||||
return drainedEntries
|
||||
}
|
||||
|
||||
export function clearAllSkips(): void {
|
||||
fsyncSkips.length = 0
|
||||
}
|
||||
Reference in New Issue
Block a user