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