@filen/sync 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -1
- package/dist/ignorer.d.ts +6 -0
- package/dist/ignorer.js +43 -24
- package/dist/ignorer.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/deltas.d.ts +58 -2
- package/dist/lib/deltas.js +693 -108
- package/dist/lib/deltas.js.map +1 -1
- package/dist/lib/environment.d.ts +47 -0
- package/dist/lib/environment.js +71 -0
- package/dist/lib/environment.js.map +1 -0
- package/dist/lib/filesystems/dirTree.d.ts +70 -0
- package/dist/lib/filesystems/dirTree.js +157 -0
- package/dist/lib/filesystems/dirTree.js.map +1 -0
- package/dist/lib/filesystems/local.d.ts +18 -8
- package/dist/lib/filesystems/local.js +166 -160
- package/dist/lib/filesystems/local.js.map +1 -1
- package/dist/lib/filesystems/remote.d.ts +12 -5
- package/dist/lib/filesystems/remote.js +226 -172
- package/dist/lib/filesystems/remote.js.map +1 -1
- package/dist/lib/ipc.js +1 -2
- package/dist/lib/ipc.js.map +1 -1
- package/dist/lib/lock.js +19 -12
- package/dist/lib/lock.js.map +1 -1
- package/dist/lib/logger.js +9 -7
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/state.js +159 -63
- package/dist/lib/state.js.map +1 -1
- package/dist/lib/sync.d.ts +18 -0
- package/dist/lib/sync.js +165 -96
- package/dist/lib/sync.js.map +1 -1
- package/dist/lib/tasks.d.ts +7 -8
- package/dist/lib/tasks.js +38 -45
- package/dist/lib/tasks.js.map +1 -1
- package/dist/semaphore.d.ts +1 -0
- package/dist/semaphore.js +22 -5
- package/dist/semaphore.js.map +1 -1
- package/dist/utils.js +51 -35
- package/dist/utils.js.map +1 -1
- package/eslint.config.mjs +36 -0
- package/package.json +19 -15
- package/tests/bench/collapse.bench.ts +114 -0
- package/tests/bench/cycle.bench.ts +111 -0
- package/tests/bench/deltas.bench.ts +151 -0
- package/tests/bench/harness/fake-sync.ts +32 -0
- package/tests/bench/harness/measure.ts +276 -0
- package/tests/bench/harness/scale-world.ts +160 -0
- package/tests/bench/harness/trees.ts +275 -0
- package/tests/bench/local-scan.bench.ts +74 -0
- package/tests/bench/longrun.bench.ts +130 -0
- package/tests/bench/profile-incremental.ts +90 -0
- package/tests/bench/remote-build.bench.ts +104 -0
- package/tests/bench/render.ts +14 -0
- package/tests/bench/semaphore.bench.ts +79 -0
- package/tests/bench/state.bench.ts +85 -0
- package/tests/bench/tasks-dispatch.bench.ts +156 -0
- package/tests/conformance/virtual-fs.test.ts +213 -0
- package/tests/e2e/backup.e2e.test.ts +130 -0
- package/tests/e2e/confirm.e2e.test.ts +191 -0
- package/tests/e2e/conflict.e2e.test.ts +261 -0
- package/tests/e2e/edge.e2e.test.ts +339 -0
- package/tests/e2e/harness/account.ts +104 -0
- package/tests/e2e/harness/assert.ts +127 -0
- package/tests/e2e/harness/drive.ts +88 -0
- package/tests/e2e/harness/mutations.ts +249 -0
- package/tests/e2e/harness/world.ts +222 -0
- package/tests/e2e/ignore.e2e.test.ts +123 -0
- package/tests/e2e/lifecycle.e2e.test.ts +290 -0
- package/tests/e2e/modes.e2e.test.ts +215 -0
- package/tests/e2e/platform.e2e.test.ts +157 -0
- package/tests/e2e/property.e2e.test.ts +163 -0
- package/tests/e2e/races.e2e.test.ts +90 -0
- package/tests/e2e/regressions.e2e.test.ts +212 -0
- package/tests/e2e/resilience.e2e.test.ts +231 -0
- package/tests/e2e/special.e2e.test.ts +185 -0
- package/tests/e2e/state.e2e.test.ts +229 -0
- package/tests/e2e/sync.e2e.test.ts +222 -0
- package/tests/fakes/fake-cloud.test.ts +267 -0
- package/tests/fakes/fake-cloud.ts +1094 -0
- package/tests/fakes/virtual-fs.ts +354 -0
- package/tests/harness/known-bug.ts +17 -0
- package/tests/harness/mutations.ts +65 -0
- package/tests/harness/runner.ts +141 -0
- package/tests/harness/snapshot.ts +113 -0
- package/tests/harness/world.ts +187 -0
- package/tests/scenarios/a-baseline.test.ts +107 -0
- package/tests/scenarios/aa-races.test.ts +258 -0
- package/tests/scenarios/ab-mode-property.test.ts +189 -0
- package/tests/scenarios/ac-platform.test.ts +320 -0
- package/tests/scenarios/ad-unicode-normalization.test.ts +67 -0
- package/tests/scenarios/b-additions.test.ts +160 -0
- package/tests/scenarios/c-modifications.test.ts +194 -0
- package/tests/scenarios/d-deletions.test.ts +259 -0
- package/tests/scenarios/e-rename-move.test.ts +288 -0
- package/tests/scenarios/f-ignore-filter.test.ts +346 -0
- package/tests/scenarios/g-large-deletion.test.ts +277 -0
- package/tests/scenarios/h-resilience.test.ts +167 -0
- package/tests/scenarios/i-lifecycle.test.ts +353 -0
- package/tests/scenarios/j-state-cache.test.ts +264 -0
- package/tests/scenarios/k-scale.test.ts +202 -0
- package/tests/scenarios/l-property.test.ts +145 -0
- package/tests/scenarios/m-golden.test.ts +452 -0
- package/tests/scenarios/o-task-errors.test.ts +497 -0
- package/tests/scenarios/p-remote-originated.test.ts +306 -0
- package/tests/scenarios/q-cycle-lifecycle.test.ts +234 -0
- package/tests/scenarios/r-rename-stress.test.ts +208 -0
- package/tests/scenarios/s-upgrade-transition.test.ts +171 -0
- package/tests/scenarios/t-type-change.test.ts +144 -0
- package/tests/scenarios/u-mode-local-to-cloud.test.ts +347 -0
- package/tests/scenarios/v-mode-local-backup.test.ts +201 -0
- package/tests/scenarios/w-mode-cloud-to-local.test.ts +304 -0
- package/tests/scenarios/x-mode-cloud-backup.test.ts +201 -0
- package/tests/scenarios/y-conflict-matrix.test.ts +292 -0
- package/tests/scenarios/z-cross-ops.test.ts +285 -0
- package/tests/scenarios/zb-dir-rename-cross.test.ts +296 -0
- package/tests/scenarios/zc-crash-recovery.test.ts +189 -0
- package/tests/scenarios/zd-inode-reuse.test.ts +118 -0
- package/tests/scenarios/ze-move-into-new-dir.test.ts +130 -0
- package/tests/scenarios/zf-remote-change-unchanged-local.test.ts +81 -0
- package/tests/scenarios/zg-edit-during-scan.test.ts +68 -0
- package/tests/scenarios/zh-dir-delete-vs-child.test.ts +104 -0
- package/tests/scenarios/zi-smoke-test-outage.test.ts +78 -0
- package/tests/scenarios/zj-trash-cleanup.test.ts +133 -0
- package/tests/scenarios/zk-ignore-asymmetry.test.ts +150 -0
- package/tests/scenarios/zl-mode-atomicity.test.ts +104 -0
- package/tests/scenarios/zm-scan-concurrency.test.ts +78 -0
- package/tests/scenarios/zn-delta-ordering.test.ts +130 -0
- package/tests/scenarios/zo-download-temp-cleanup.test.ts +65 -0
- package/tests/unit/collapse-deltas.test.ts +276 -0
- package/tests/unit/dir-tree.test.ts +159 -0
- package/tests/unit/icloud.test.ts +115 -0
- package/tests/unit/ignorer-cache-regression.test.ts +70 -0
- package/tests/unit/ignorer.test.ts +63 -0
- package/tests/unit/ipc-lock.test.ts +438 -0
- package/tests/unit/lock.test.ts +135 -0
- package/tests/unit/n-unit.test.ts +632 -0
- package/tests/unit/remote-tree-unordered-regression.test.ts +101 -0
- package/tests/unit/semaphore-regression.test.ts +140 -0
- package/tests/unit/state-refencode-regression.test.ts +224 -0
- package/tests/unit/state.test.ts +809 -0
- package/tests/unit/tasks-dispatch-order-regression.test.ts +53 -0
- package/tests/unit/worker-api.test.ts +379 -0
- package/tsconfig.json +10 -1
- package/tsconfig.test.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.bench.config.ts +32 -0
- package/vitest.config.ts +27 -0
- package/vitest.e2e.config.ts +68 -0
- package/.eslintrc +0 -16
- package/jest.config.js +0 -5
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { APIError } from "@filen/sdk"
|
|
3
|
+
import { createFakeCloud, type CloudSpec } from "./fake-cloud"
|
|
4
|
+
import { createVirtualFS, type VfsSpec } from "./virtual-fs"
|
|
5
|
+
|
|
6
|
+
function setup(localSpec: VfsSpec = {}, cloudSpec: CloudSpec = {}) {
|
|
7
|
+
const vfs = createVirtualFS(localSpec)
|
|
8
|
+
const cloud = createFakeCloud(cloudSpec, { localFs: vfs.fs })
|
|
9
|
+
|
|
10
|
+
return { vfs, cloud, sdk: cloud.sdk, controls: cloud.controls }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("fake cloud — tree, cache, tuples", () => {
|
|
14
|
+
it("returns the root folder with parent 'base' and no files for an empty cloud", async () => {
|
|
15
|
+
const { sdk, controls } = setup()
|
|
16
|
+
|
|
17
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })
|
|
18
|
+
|
|
19
|
+
expect(tree.files).toEqual([])
|
|
20
|
+
expect(tree.folders).toHaveLength(1)
|
|
21
|
+
expect(tree.folders[0]![0]).toBe(controls.rootUUID)
|
|
22
|
+
expect(tree.folders[0]![2]).toBe("base")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("emits the canonical file tuple order [uuid,bucket,region,chunks,parent,metadata,version,n]", async () => {
|
|
26
|
+
const { vfs, sdk, controls } = setup({ "/src/a.txt": "hello world" })
|
|
27
|
+
|
|
28
|
+
const item = await sdk.cloud().uploadLocalFile({ source: "/src/a.txt", parent: controls.rootUUID, name: "a.txt" })
|
|
29
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
30
|
+
const fileTuple = tree.files.find(file => file[0] === item.uuid)!
|
|
31
|
+
|
|
32
|
+
expect(fileTuple).toBeDefined()
|
|
33
|
+
expect(fileTuple[0]).toBe(item.uuid) // uuid
|
|
34
|
+
expect(fileTuple[1]).toBe("filen-1") // bucket
|
|
35
|
+
expect(fileTuple[2]).toBe("de-1") // region
|
|
36
|
+
expect(fileTuple[3]).toBe(1) // chunks (11 bytes → 1 chunk)
|
|
37
|
+
expect(fileTuple[4]).toBe(controls.rootUUID) // parent
|
|
38
|
+
expect(fileTuple[6]).toBe(2) // version
|
|
39
|
+
|
|
40
|
+
expect(item.type).toBe("file")
|
|
41
|
+
|
|
42
|
+
const decrypted = await sdk.crypto().decrypt().fileMetadata({ metadata: fileTuple[5] })
|
|
43
|
+
|
|
44
|
+
expect(decrypted.name).toBe("a.txt")
|
|
45
|
+
expect(decrypted.size).toBe("hello world".length)
|
|
46
|
+
|
|
47
|
+
if (item.type === "file") {
|
|
48
|
+
expect(decrypted.key).toBe(item.key)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// uploadLocalFile reads the bytes from the injected virtual filesystem.
|
|
52
|
+
expect(item.size).toBe("hello world".length)
|
|
53
|
+
void vfs
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("honors the per-deviceId revision cache (unchanged → empty, skipCache → full, new device → full)", async () => {
|
|
57
|
+
const { sdk, controls } = setup()
|
|
58
|
+
|
|
59
|
+
await sdk.cloud().createDirectory({ name: "docs", parent: controls.rootUUID })
|
|
60
|
+
|
|
61
|
+
// First fetch for device-1 → full tree.
|
|
62
|
+
const first = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })
|
|
63
|
+
expect(first.folders.length).toBe(2)
|
|
64
|
+
|
|
65
|
+
// Nothing changed since device-1 last fetched → empty "unchanged" response.
|
|
66
|
+
const second = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })
|
|
67
|
+
expect(second.files).toEqual([])
|
|
68
|
+
expect(second.folders).toEqual([])
|
|
69
|
+
|
|
70
|
+
// skipCache always returns the full tree.
|
|
71
|
+
const skip = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
72
|
+
expect(skip.folders.length).toBe(2)
|
|
73
|
+
|
|
74
|
+
// A different device has never fetched → full tree.
|
|
75
|
+
const otherDevice = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-2" })
|
|
76
|
+
expect(otherDevice.folders.length).toBe(2)
|
|
77
|
+
|
|
78
|
+
// A mutation invalidates device-1's cache.
|
|
79
|
+
await sdk.cloud().createDirectory({ name: "more", parent: controls.rootUUID })
|
|
80
|
+
const afterChange = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })
|
|
81
|
+
expect(afterChange.folders.length).toBe(3)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("fake cloud — directories, decrypt", () => {
|
|
86
|
+
it("createDirectory adds a folder tuple and is idempotent by name", async () => {
|
|
87
|
+
const { sdk, controls } = setup()
|
|
88
|
+
|
|
89
|
+
const uuid1 = await sdk.cloud().createDirectory({ name: "docs", parent: controls.rootUUID })
|
|
90
|
+
const uuid2 = await sdk.cloud().createDirectory({ name: "docs", parent: controls.rootUUID })
|
|
91
|
+
|
|
92
|
+
expect(uuid2).toBe(uuid1)
|
|
93
|
+
|
|
94
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
95
|
+
const folderTuple = tree.folders.find(folder => folder[0] === uuid1)!
|
|
96
|
+
|
|
97
|
+
expect(folderTuple[2]).toBe(controls.rootUUID)
|
|
98
|
+
|
|
99
|
+
const decrypted = await sdk.crypto().decrypt().folderMetadata({ metadata: folderTuple[1] })
|
|
100
|
+
expect(decrypted.name).toBe("docs")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("decrypt round-trips JSON metadata for files and folders", async () => {
|
|
104
|
+
const { sdk, controls } = setup({ "/src/note.md": "content" })
|
|
105
|
+
|
|
106
|
+
const dirUUID = await sdk.cloud().createDirectory({ name: "folder", parent: controls.rootUUID })
|
|
107
|
+
const fileItem = await sdk.cloud().uploadLocalFile({ source: "/src/note.md", parent: dirUUID, name: "note.md" })
|
|
108
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
109
|
+
|
|
110
|
+
const folderTuple = tree.folders.find(folder => folder[0] === dirUUID)!
|
|
111
|
+
const fileTuple = tree.files.find(file => file[0] === fileItem.uuid)!
|
|
112
|
+
|
|
113
|
+
expect((await sdk.crypto().decrypt().folderMetadata({ metadata: folderTuple[1] })).name).toBe("folder")
|
|
114
|
+
|
|
115
|
+
const fileMeta = await sdk.crypto().decrypt().fileMetadata({ metadata: fileTuple[5] })
|
|
116
|
+
expect(fileMeta.name).toBe("note.md")
|
|
117
|
+
expect(fileMeta.mime).toBe("text/markdown")
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe("fake cloud — case-insensitive uniqueness & versioning", () => {
|
|
122
|
+
it("errors on a file/folder type clash in either direction", async () => {
|
|
123
|
+
const { sdk, controls } = setup({ "/src/x": "data" })
|
|
124
|
+
|
|
125
|
+
await sdk.cloud().createDirectory({ name: "X", parent: controls.rootUUID })
|
|
126
|
+
|
|
127
|
+
await expect(sdk.cloud().uploadLocalFile({ source: "/src/x", parent: controls.rootUUID, name: "x" })).rejects.toThrow()
|
|
128
|
+
|
|
129
|
+
const { vfs, sdk: sdk2, controls: controls2 } = setup({ "/src/y": "data" })
|
|
130
|
+
await sdk2.cloud().uploadLocalFile({ source: "/src/y", parent: controls2.rootUUID, name: "y" })
|
|
131
|
+
await expect(sdk2.cloud().createDirectory({ name: "Y", parent: controls2.rootUUID })).rejects.toThrow()
|
|
132
|
+
void vfs
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("versions a file on same-name re-upload: a fresh uuid supersedes and the old leaves the tree", async () => {
|
|
136
|
+
const { vfs, sdk, controls } = setup({ "/src/a.txt": "v1" })
|
|
137
|
+
|
|
138
|
+
const first = await sdk.cloud().uploadLocalFile({ source: "/src/a.txt", parent: controls.rootUUID, name: "a.txt" })
|
|
139
|
+
|
|
140
|
+
await vfs.fs.writeFile("/src/a.txt", "v2-longer", { encoding: "utf-8" })
|
|
141
|
+
const second = await sdk.cloud().uploadLocalFile({ source: "/src/a.txt", parent: controls.rootUUID, name: "a.txt" })
|
|
142
|
+
|
|
143
|
+
expect(second.uuid).not.toBe(first.uuid)
|
|
144
|
+
|
|
145
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
146
|
+
const matching = tree.files.filter(file => file[4] === controls.rootUUID)
|
|
147
|
+
expect(matching).toHaveLength(1)
|
|
148
|
+
expect(matching[0]![0]).toBe(second.uuid)
|
|
149
|
+
|
|
150
|
+
// The superseded uuid is gone from the sync perspective.
|
|
151
|
+
expect((await sdk.api(3).dir().present({ uuid: first.uuid })).present).toBe(false)
|
|
152
|
+
expect((await sdk.api(3).dir().present({ uuid: second.uuid })).present).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe("fake cloud — trash, delete, present", () => {
|
|
157
|
+
it("reports trashed as present+trash:true and permanently deleted as not present", async () => {
|
|
158
|
+
const { sdk, controls } = setup({ "/a.txt": "data" }, { "/cloudfile.txt": "x" })
|
|
159
|
+
|
|
160
|
+
const node = controls.getByPath("/cloudfile.txt")!
|
|
161
|
+
|
|
162
|
+
await sdk.cloud().trashFile({ uuid: node.uuid })
|
|
163
|
+
expect(await sdk.api(3).dir().present({ uuid: node.uuid })).toEqual({ present: true, trash: true })
|
|
164
|
+
|
|
165
|
+
// Trashed nodes drop out of the tree.
|
|
166
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
167
|
+
expect(tree.files).toHaveLength(0)
|
|
168
|
+
|
|
169
|
+
await sdk.cloud().deleteFile({ uuid: node.uuid })
|
|
170
|
+
expect(await sdk.api(3).dir().present({ uuid: node.uuid })).toEqual({ present: false, trash: false })
|
|
171
|
+
void controls
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("raises APIError(file_not_found / folder_not_found) on gone uuids", async () => {
|
|
175
|
+
const { sdk } = setup()
|
|
176
|
+
|
|
177
|
+
await expect(sdk.cloud().trashFile({ uuid: "missing" })).rejects.toBeInstanceOf(APIError)
|
|
178
|
+
await expect(sdk.cloud().deleteFile({ uuid: "missing" })).rejects.toMatchObject({ code: "file_not_found" })
|
|
179
|
+
await expect(sdk.cloud().deleteDirectory({ uuid: "missing" })).rejects.toMatchObject({ code: "folder_not_found" })
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe("fake cloud — rename/move overwriteIfExists & fileExists", () => {
|
|
184
|
+
it("trashes the occupant when overwriteIfExists is set, and throws otherwise", async () => {
|
|
185
|
+
const { sdk, controls } = setup({}, { "/a.txt": "A", "/b.txt": "B" })
|
|
186
|
+
|
|
187
|
+
const a = controls.getByPath("/a.txt")!
|
|
188
|
+
const b = controls.getByPath("/b.txt")!
|
|
189
|
+
|
|
190
|
+
await expect(
|
|
191
|
+
sdk.cloud().renameFile({ uuid: b.uuid, metadata: { name: "b.txt", size: 1, mime: "x", key: "k", lastModified: 1 }, name: "a.txt" })
|
|
192
|
+
).rejects.toThrow()
|
|
193
|
+
|
|
194
|
+
await sdk.cloud().renameFile({
|
|
195
|
+
uuid: b.uuid,
|
|
196
|
+
metadata: { name: "b.txt", size: 1, mime: "x", key: "k", lastModified: 1 },
|
|
197
|
+
name: "a.txt",
|
|
198
|
+
overwriteIfExists: true
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect((await sdk.api(3).dir().present({ uuid: a.uuid })).trash).toBe(true)
|
|
202
|
+
|
|
203
|
+
const tree = await sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1", skipCache: true })
|
|
204
|
+
expect(tree.files).toHaveLength(1)
|
|
205
|
+
expect(tree.files[0]![0]).toBe(b.uuid)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("fileExists distinguishes files from directories", async () => {
|
|
209
|
+
const { sdk, controls } = setup({}, { "/a.txt": "A", "/dir": null })
|
|
210
|
+
|
|
211
|
+
const a = controls.getByPath("/a.txt")!
|
|
212
|
+
|
|
213
|
+
expect(await sdk.cloud().fileExists({ name: "a.txt", parent: controls.rootUUID })).toEqual({ exists: true, uuid: a.uuid })
|
|
214
|
+
expect(await sdk.cloud().fileExists({ name: "missing.txt", parent: controls.rootUUID })).toEqual({ exists: false })
|
|
215
|
+
expect(await sdk.cloud().fileExists({ name: "dir", parent: controls.rootUUID })).toEqual({ exists: false })
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe("fake cloud — locks, error injection", () => {
|
|
220
|
+
it("grants an uncontended lock and rejects while held by another holder", async () => {
|
|
221
|
+
const { sdk } = setup()
|
|
222
|
+
|
|
223
|
+
await sdk.user().acquireResourceLock({ resource: "r", lockUUID: "holder-1" })
|
|
224
|
+
await expect(sdk.user().acquireResourceLock({ resource: "r", lockUUID: "holder-2" })).rejects.toThrow()
|
|
225
|
+
|
|
226
|
+
await sdk.user().releaseResourceLock({ resource: "r", lockUUID: "holder-1" })
|
|
227
|
+
await expect(sdk.user().acquireResourceLock({ resource: "r", lockUUID: "holder-2" })).resolves.toBeUndefined()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("simulates external contention via controls.contendLock", async () => {
|
|
231
|
+
const { sdk, controls } = setup()
|
|
232
|
+
|
|
233
|
+
controls.contendLock("r")
|
|
234
|
+
await expect(sdk.user().acquireResourceLock({ resource: "r", lockUUID: "me" })).rejects.toThrow()
|
|
235
|
+
|
|
236
|
+
controls.releaseLockContention("r")
|
|
237
|
+
await expect(sdk.user().acquireResourceLock({ resource: "r", lockUUID: "me" })).resolves.toBeUndefined()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("throws an injected error from the named method", async () => {
|
|
241
|
+
const { sdk, controls } = setup()
|
|
242
|
+
|
|
243
|
+
controls.setError("tree", new Error("boom"))
|
|
244
|
+
await expect(sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })).rejects.toThrow("boom")
|
|
245
|
+
|
|
246
|
+
controls.clearError("tree")
|
|
247
|
+
await expect(sdk.api(3).dir().tree({ uuid: controls.rootUUID, deviceId: "device-1" })).resolves.toBeDefined()
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe("fake cloud — snapshot & external mutators", () => {
|
|
252
|
+
it("reflects external mutations in the normalized snapshot", async () => {
|
|
253
|
+
const { controls } = setup({}, { "/keep.txt": "keep" })
|
|
254
|
+
|
|
255
|
+
controls.addDir("/photos")
|
|
256
|
+
controls.addFile("/photos/p.txt", "pixels", { mtimeMs: 1_700_000_000_000 })
|
|
257
|
+
controls.movePath("/keep.txt", "/photos/kept.txt")
|
|
258
|
+
|
|
259
|
+
const snapshot = controls.snapshot()
|
|
260
|
+
|
|
261
|
+
expect(snapshot["/photos"]).toEqual({ type: "directory", size: 0, mtimeSec: 0, contentHash: "" })
|
|
262
|
+
expect(snapshot["/photos/p.txt"]!.type).toBe("file")
|
|
263
|
+
expect(snapshot["/photos/p.txt"]!.mtimeSec).toBe(1_700_000_000)
|
|
264
|
+
expect(snapshot["/photos/kept.txt"]!.type).toBe("file")
|
|
265
|
+
expect(snapshot["/keep.txt"]).toBeUndefined()
|
|
266
|
+
})
|
|
267
|
+
})
|