import { type E2EWorld } from "./world" import pathModule from "path" import fs from "fs-extra" import { v4 as uuidv4 } from "uuid" /** Resolve a root-relative path (with or without a leading slash) to an absolute local path. */ function abs(world: E2EWorld, relativePath: string): string { return pathModule.join(world.localRoot, relativePath.replace(/^\/+/, "")) } // ---- Local mutations (real filesystem under the sync root) ------------------------------------------- export async function writeLocal(world: E2EWorld, relativePath: string, content: string | Uint8Array): Promise { const full = abs(world, relativePath) await fs.ensureDir(pathModule.dirname(full)) await fs.writeFile(full, content) } /** * Modify an existing file's content AND stamp it with a clearly-newer mtime. The engine compares * whole-second mtimes (plus size), so a same-size edit that lands in the same second as the previous * version is — by design — invisible to it. Tests that specifically assert "a modification propagates" * use this to make the change deterministically detectable regardless of how fast the prior cycle ran. */ export async function modifyLocal(world: E2EWorld, relativePath: string, content: string): Promise { const full = abs(world, relativePath) await fs.ensureDir(pathModule.dirname(full)) await fs.writeFile(full, content) const newer = new Date(Date.now() + 5000) await fs.utimes(full, newer, newer) } /** * Overwrite a file with new content while RESTORING its previous mtime, reproducing an edit that * lands in the same whole-second as the last sync (the exact E2E-OBS-002 condition). The whole-second * mtime is unchanged, so only base-relative SIZE comparison can detect the edit. */ export async function writeLocalPreservingMtime(world: E2EWorld, relativePath: string, content: string): Promise { const full = abs(world, relativePath) const { atime, mtime } = await fs.stat(full) await fs.writeFile(full, content) await fs.utimes(full, atime, mtime) } export async function readLocal(world: E2EWorld, relativePath: string): Promise { return await fs.readFile(abs(world, relativePath), { encoding: "utf-8" }) } export async function mkdirLocal(world: E2EWorld, relativePath: string): Promise { await fs.ensureDir(abs(world, relativePath)) } export async function rmLocal(world: E2EWorld, relativePath: string): Promise { await fs.rm(abs(world, relativePath), { force: true, recursive: true, maxRetries: 10, retryDelay: 100 }) } export async function renameLocal(world: E2EWorld, fromRelative: string, toRelative: string): Promise { const to = abs(world, toRelative) await fs.ensureDir(pathModule.dirname(to)) await fs.move(abs(world, fromRelative), to, { overwrite: true }) } export async function existsLocal(world: E2EWorld, relativePath: string): Promise { return await fs.pathExists(abs(world, relativePath)) } /** * Create a symlink at `linkRelativePath` pointing at `target` (raw link contents — typically an * absolute path). Throws on platforms/permissions that forbid symlink creation (e.g. Windows without * Developer Mode); callers skip the test in that case. */ export async function symlinkLocal(world: E2EWorld, linkRelativePath: string, target: string): Promise { const full = abs(world, linkRelativePath) await fs.ensureDir(pathModule.dirname(full)) await fs.symlink(target, full) } /** Create a HARD link at `linkRelativePath` pointing at the same inode as the existing `targetRelativePath`. */ export async function linkLocal(world: E2EWorld, targetRelativePath: string, linkRelativePath: string): Promise { const link = abs(world, linkRelativePath) await fs.ensureDir(pathModule.dirname(link)) await fs.link(abs(world, targetRelativePath), link) } // ---- Remote mutations (real SDK calls under the / root) --------------------------------------- const segments = (relativePath: string): string[] => relativePath.split("/").map(segment => segment.trim()).filter(Boolean) /** * Find-or-create the directory chain for a root-relative directory path, returning the leaf directory's * uuid. An empty path resolves to the remote root itself. */ export async function ensureRemoteDir(world: E2EWorld, relativeDir: string): Promise { let parentUUID = world.remoteParentUUID for (const segment of segments(relativeDir)) { const items = await world.sdk.cloud().listDirectory({ uuid: parentUUID }) const existing = items.find(item => item.type === "directory" && item.name === segment) parentUUID = existing ? existing.uuid : await world.sdk.cloud().createDirectory({ name: segment, parent: parentUUID }) } return parentUUID } export async function mkdirRemote(world: E2EWorld, relativePath: string): Promise { return await ensureRemoteDir(world, relativePath) } /** Upload `content` to the remote at `relativePath`, creating any missing parent directories. */ export async function uploadRemote(world: E2EWorld, relativePath: string, content: string): Promise { const parts = segments(relativePath) const name = parts[parts.length - 1] if (!name) { throw new Error(`Invalid remote upload path: ${relativePath}`) } const parentUUID = await ensureRemoteDir(world, parts.slice(0, -1).join("/")) const source = pathModule.join(world.workRoot, `upload-${uuidv4()}.tmp`) await fs.writeFile(source, content) try { await world.sdk.cloud().uploadLocalFile({ source, parent: parentUUID, name }) } finally { await fs.rm(source, { force: true, maxRetries: 5, retryDelay: 50 }).catch(() => {}) } } /** The CloudItem at a root-relative path, or null if absent. */ export async function resolveRemote( world: E2EWorld, relativePath: string ): Promise["listDirectory"]>>[number] | null> { const parts = segments(relativePath) if (parts.length === 0) { return null } let parentUUID = world.remoteParentUUID for (let i = 0; i < parts.length; i++) { const items = await world.sdk.cloud().listDirectory({ uuid: parentUUID }) const match = items.find(item => item.name === parts[i]) if (!match) { return null } if (i === parts.length - 1) { return match } if (match.type !== "directory") { return null } parentUUID = match.uuid } return null } /** * Rename a remote DIRECTORY within its current parent (simulating a peer client's rename), via the SDK * renameDirectory call the engine itself uses. */ export async function renameRemoteDir(world: E2EWorld, fromRelative: string, toRelative: string): Promise { const item = await resolveRemote(world, fromRelative) if (!item || item.type !== "directory") { throw new Error(`renameRemoteDir: no remote directory at ${fromRelative}`) } const newName = segments(toRelative).pop() if (!newName) { throw new Error(`renameRemoteDir: invalid target ${toRelative}`) } await world.sdk.cloud().renameDirectory({ uuid: item.uuid, name: newName, overwriteIfExists: true }) } /** * Rename a remote FILE within its current directory (simulating a peer client's rename), via the SDK * renameFile call the engine itself uses. Only same-directory file renames are needed by the conflict * suite, so the metadata is rebuilt from the resolved item. */ export async function renameRemote(world: E2EWorld, fromRelative: string, toRelative: string): Promise { const item = await resolveRemote(world, fromRelative) if (!item || item.type !== "file") { throw new Error(`renameRemote: no remote file at ${fromRelative}`) } const newName = segments(toRelative).pop() if (!newName) { throw new Error(`renameRemote: invalid target ${toRelative}`) } await world.sdk.cloud().renameFile({ uuid: item.uuid, name: newName, metadata: { name: newName, size: item.size, mime: item.mime, key: item.key, lastModified: item.lastModified, ...(item.creation !== undefined ? { creation: item.creation } : {}), ...(item.hash !== undefined ? { hash: item.hash } : {}) }, overwriteIfExists: true }) } /** * Move a remote item ACROSS directories (optionally renaming it too), simulating a peer client's move. * Mirrors the engine's own remote move sequence — renameFile/renameDirectory if the basename changes, THEN * moveFile/moveDirectory to the new parent — so the live behavior matches what `controls.movePath` models in * the mock. `overwriteIfExists` so a move onto an occupied name trashes the occupant (per-parent uniqueness), * exactly as the engine's moves do. (renameRemote/renameRemoteDir only cover SAME-directory renames; this * closes the cross-directory remote-move parity gap.) */ export async function moveRemote(world: E2EWorld, fromRelative: string, toRelative: string): Promise { const item = await resolveRemote(world, fromRelative) if (!item) { throw new Error(`moveRemote: no remote item at ${fromRelative}`) } const fromParts = segments(fromRelative) const oldName = fromParts.pop() const sourceDir = fromParts.join("/") const toParts = segments(toRelative) const newName = toParts.pop() const destDir = toParts.join("/") if (!oldName || !newName) { throw new Error(`moveRemote: invalid paths ${fromRelative} -> ${toRelative}`) } const newParentUUID = await ensureRemoteDir(world, destDir) const crossDir = sourceDir !== destDir if (item.type === "directory") { if (newName !== oldName) { await world.sdk.cloud().renameDirectory({ uuid: item.uuid, name: newName, overwriteIfExists: true }) } if (crossDir) { await world.sdk.cloud().moveDirectory({ uuid: item.uuid, to: newParentUUID, metadata: { name: newName }, overwriteIfExists: true }) } return } const metadata = { name: newName, size: item.size, mime: item.mime, key: item.key, lastModified: item.lastModified, ...(item.creation !== undefined ? { creation: item.creation } : {}), ...(item.hash !== undefined ? { hash: item.hash } : {}) } if (newName !== oldName) { await world.sdk.cloud().renameFile({ uuid: item.uuid, metadata, name: newName, overwriteIfExists: true }) } if (crossDir) { await world.sdk.cloud().moveFile({ uuid: item.uuid, to: newParentUUID, metadata, overwriteIfExists: true }) } } /** Force a local file's mtime to a specific epoch-ms (e.g. to age it before a newer conflicting write). */ export async function setLocalMtime(world: E2EWorld, relativePath: string, epochMs: number): Promise { const when = new Date(epochMs) await fs.utimes(abs(world, relativePath), when, when) } /** Change a local file's permission bits (e.g. 0o000 to make it unreadable for a fault-tolerance test). */ export async function chmodLocal(world: E2EWorld, relativePath: string, mode: number): Promise { await fs.chmod(abs(world, relativePath), mode) } /** Permanently delete (no trash) the remote item at `relativePath`. */ export async function deleteRemote(world: E2EWorld, relativePath: string): Promise { const item = await resolveRemote(world, relativePath) if (!item) { return } if (item.type === "directory") { await world.sdk.cloud().deleteDirectory({ uuid: item.uuid }) } else { await world.sdk.cloud().deleteFile({ uuid: item.uuid }) } }