pinokiod 8.0.73 → 8.0.74
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.
|
@@ -52,7 +52,6 @@ class AutomaticScans {
|
|
|
52
52
|
this.manualDepth = 0
|
|
53
53
|
this.observedApps = new Set()
|
|
54
54
|
this.changedPaths = new Map()
|
|
55
|
-
this.changedPathIndex = new Map()
|
|
56
55
|
this.pendingStops = new Map()
|
|
57
56
|
this.lifecycleWork = new Set()
|
|
58
57
|
this.stopSettleMs = STOP_SETTLE_MS
|
|
@@ -138,7 +137,6 @@ class AutomaticScans {
|
|
|
138
137
|
}
|
|
139
138
|
this.observedApps.clear()
|
|
140
139
|
this.changedPaths.clear()
|
|
141
|
-
this.changedPathIndex.clear()
|
|
142
140
|
if (watcherError) throw watcherError
|
|
143
141
|
}
|
|
144
142
|
|
|
@@ -151,117 +149,123 @@ class AutomaticScans {
|
|
|
151
149
|
})
|
|
152
150
|
return
|
|
153
151
|
}
|
|
154
|
-
|
|
152
|
+
const watcherEvents = Array.isArray(events) ? events : []
|
|
153
|
+
if (!watcherEvents.some((event) => event && event.type === "delete")) {
|
|
154
|
+
for (const event of watcherEvents) {
|
|
155
|
+
if (event && ["create", "update"].includes(event.type)) {
|
|
156
|
+
this.recordChangedPath(event.path)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const batches = new Map()
|
|
162
|
+
for (let index = 0; index < watcherEvents.length; index++) {
|
|
163
|
+
const event = watcherEvents[index]
|
|
155
164
|
if (!event || !["create", "update", "delete"].includes(event.type)) {
|
|
156
165
|
continue
|
|
157
166
|
}
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
const target = this.changedPathTarget(
|
|
168
|
+
event.path, event.type === "delete")
|
|
169
|
+
if (!target) continue
|
|
170
|
+
let batch = batches.get(target.app)
|
|
171
|
+
if (!batch) {
|
|
172
|
+
batch = {
|
|
173
|
+
root: target.root,
|
|
174
|
+
updates: new Map(),
|
|
175
|
+
deletes: new Map()
|
|
176
|
+
}
|
|
177
|
+
batches.set(target.app, batch)
|
|
178
|
+
}
|
|
179
|
+
const key = pathKey(target.path)
|
|
180
|
+
if (event.type === "delete") batch.deletes.set(key, index)
|
|
181
|
+
else batch.updates.set(key, { index, path: target.path })
|
|
182
|
+
}
|
|
183
|
+
for (const [app, batch] of batches) {
|
|
184
|
+
this.applyChangedPathBatch(app, batch)
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
|
|
188
|
+
changedPathTarget(filePath, allowRoot = false) {
|
|
164
189
|
if (this.disposed || !this.supported ||
|
|
165
190
|
typeof filePath !== "string" || !filePath) {
|
|
166
|
-
return
|
|
191
|
+
return null
|
|
167
192
|
}
|
|
168
193
|
const app = this.appForLaunchPath(filePath)
|
|
169
|
-
if (!app || !this.observedApps.has(app)) return
|
|
170
|
-
const
|
|
194
|
+
if (!app || !this.observedApps.has(app)) return null
|
|
195
|
+
const root = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
171
196
|
const resolved = path.resolve(filePath)
|
|
172
|
-
if (resolved ===
|
|
197
|
+
if ((!allowRoot && resolved === root) || !inside(root, resolved)) return null
|
|
198
|
+
return { app, root, path: resolved }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
recordChangedPath(filePath) {
|
|
202
|
+
const target = this.changedPathTarget(filePath)
|
|
203
|
+
if (!target) return false
|
|
204
|
+
const { app, path: resolved } = target
|
|
173
205
|
let paths = this.changedPaths.get(app)
|
|
174
206
|
if (!paths) {
|
|
175
207
|
paths = new Set()
|
|
176
208
|
this.changedPaths.set(app, paths)
|
|
177
209
|
}
|
|
178
|
-
let index = this.changedPathIndex.get(app)
|
|
179
|
-
if (!index) {
|
|
180
|
-
index = { exact: new Map(), descendants: new Map() }
|
|
181
|
-
this.changedPathIndex.set(app, index)
|
|
182
|
-
}
|
|
183
|
-
const key = pathKey(resolved)
|
|
184
|
-
if (index.exact.has(key)) return true
|
|
185
210
|
paths.add(resolved)
|
|
186
|
-
index.exact.set(key, resolved)
|
|
187
|
-
this.addChangedPathToIndex(index.descendants, appRoot, resolved)
|
|
188
211
|
return true
|
|
189
212
|
}
|
|
190
213
|
|
|
191
|
-
|
|
192
|
-
const rootKey = pathKey(
|
|
193
|
-
|
|
194
|
-
let
|
|
214
|
+
lastDeleteIndex(root, filePath, deletes) {
|
|
215
|
+
const rootKey = pathKey(root)
|
|
216
|
+
let current = path.resolve(filePath)
|
|
217
|
+
let latest = -1
|
|
195
218
|
while (true) {
|
|
196
219
|
const key = pathKey(current)
|
|
197
|
-
|
|
198
|
-
if (
|
|
220
|
+
const index = deletes.get(key)
|
|
221
|
+
if (index !== undefined && index > latest) latest = index
|
|
222
|
+
if (key === rootKey) return latest
|
|
199
223
|
current = path.dirname(current)
|
|
200
224
|
}
|
|
201
225
|
}
|
|
202
226
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
descendants.add(filePath)
|
|
227
|
+
applyChangedPathBatch(app, { root, updates, deletes }) {
|
|
228
|
+
const paths = this.changedPaths.get(app)
|
|
229
|
+
if (!deletes.size) {
|
|
230
|
+
const collected = paths || new Set()
|
|
231
|
+
for (const update of updates.values()) collected.add(update.path)
|
|
232
|
+
if (collected.size) this.changedPaths.set(app, collected)
|
|
233
|
+
return
|
|
211
234
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
const retained = new Set()
|
|
236
|
+
if (paths) {
|
|
237
|
+
for (const candidate of paths) {
|
|
238
|
+
const key = pathKey(candidate)
|
|
239
|
+
const update = updates.get(key)
|
|
240
|
+
if (this.lastDeleteIndex(root, candidate, deletes) >
|
|
241
|
+
(update ? update.index : -1) || retained.has(key)) {
|
|
242
|
+
paths.delete(candidate)
|
|
243
|
+
} else {
|
|
244
|
+
retained.add(key)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
221
247
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
let collected = paths
|
|
249
|
+
for (const [key, update] of updates) {
|
|
250
|
+
if (update.index <= this.lastDeleteIndex(root, update.path, deletes) ||
|
|
251
|
+
retained.has(key)) continue
|
|
252
|
+
if (!collected) collected = new Set()
|
|
253
|
+
collected.add(update.path)
|
|
254
|
+
retained.add(key)
|
|
228
255
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const paths = this.changedPaths.get(app)
|
|
232
|
-
if (!paths) return false
|
|
233
|
-
const resolved = path.resolve(filePath)
|
|
234
|
-
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
235
|
-
const index = this.changedPathIndex.get(app)
|
|
236
|
-
if (!index) return false
|
|
237
|
-
const key = pathKey(resolved)
|
|
238
|
-
const exact = index.exact.get(key)
|
|
239
|
-
const descendants = index.descendants.get(key)
|
|
240
|
-
if (!exact && !descendants) return false
|
|
241
|
-
const matches = new Set(descendants || [])
|
|
242
|
-
if (exact) matches.add(exact)
|
|
243
|
-
for (const candidate of matches) {
|
|
244
|
-
paths.delete(candidate)
|
|
245
|
-
this.removeChangedPathFromIndex(index, appRoot, candidate)
|
|
246
|
-
}
|
|
247
|
-
if (!paths.size) this.discardChangedPaths(app)
|
|
248
|
-
return true
|
|
256
|
+
if (collected && collected.size) this.changedPaths.set(app, collected)
|
|
257
|
+
else this.changedPaths.delete(app)
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
discardChangedPaths(app) {
|
|
252
261
|
this.changedPaths.delete(app)
|
|
253
|
-
this.changedPathIndex.delete(app)
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
consumeChangedPaths(app, paths) {
|
|
257
265
|
const collected = this.changedPaths.get(app)
|
|
258
266
|
if (!collected) return
|
|
259
|
-
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
260
|
-
const index = this.changedPathIndex.get(app)
|
|
261
267
|
for (const filePath of paths || []) {
|
|
262
|
-
|
|
263
|
-
if (!collected.delete(resolved) || !index) continue
|
|
264
|
-
this.removeChangedPathFromIndex(index, appRoot, resolved)
|
|
268
|
+
collected.delete(path.resolve(filePath))
|
|
265
269
|
}
|
|
266
270
|
if (!collected.size) this.discardChangedPaths(app)
|
|
267
271
|
}
|
|
@@ -931,10 +935,11 @@ class AutomaticScans {
|
|
|
931
935
|
if (reusable) {
|
|
932
936
|
counts.hash_reuses += 1
|
|
933
937
|
memory.set(key, reusable)
|
|
934
|
-
|
|
938
|
+
const verifiedEntry = Object.assign({}, currentEntry, {
|
|
935
939
|
hash: reusable
|
|
936
|
-
})
|
|
937
|
-
|
|
940
|
+
})
|
|
941
|
+
verifiedHashes.set(pathKey(entry.path), verifiedEntry)
|
|
942
|
+
return verifiedEntry
|
|
938
943
|
}
|
|
939
944
|
let verified
|
|
940
945
|
try {
|
|
@@ -961,10 +966,11 @@ class AutomaticScans {
|
|
|
961
966
|
counts.hashed += 1
|
|
962
967
|
counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
|
|
963
968
|
memory.set(key, verified.result.hash)
|
|
964
|
-
|
|
969
|
+
const verifiedEntry = Object.assign({}, currentEntry, {
|
|
965
970
|
hash: verified.result.hash
|
|
966
|
-
})
|
|
967
|
-
|
|
971
|
+
})
|
|
972
|
+
verifiedHashes.set(pathKey(entry.path), verifiedEntry)
|
|
973
|
+
return verifiedEntry
|
|
968
974
|
}
|
|
969
975
|
|
|
970
976
|
async runPrecheck(active) {
|
|
@@ -1061,7 +1067,6 @@ class AutomaticScans {
|
|
|
1061
1067
|
const preparePeers = () => {
|
|
1062
1068
|
if (!group || group.unmatched) return
|
|
1063
1069
|
group.unmatched = new Map()
|
|
1064
|
-
group.unmatchedIdentities = new Map()
|
|
1065
1070
|
for (const [hash, value] of group.candidatesByHash) {
|
|
1066
1071
|
if (value.identities.size > 1) {
|
|
1067
1072
|
for (const candidate of value.candidates) {
|
|
@@ -1073,9 +1078,6 @@ class AutomaticScans {
|
|
|
1073
1078
|
identity,
|
|
1074
1079
|
candidates: value.candidates
|
|
1075
1080
|
})
|
|
1076
|
-
group.unmatchedIdentities.set(
|
|
1077
|
-
identity,
|
|
1078
|
-
(group.unmatchedIdentities.get(identity) || 0) + 1)
|
|
1079
1081
|
}
|
|
1080
1082
|
}
|
|
1081
1083
|
}
|
|
@@ -1104,47 +1106,39 @@ class AutomaticScans {
|
|
|
1104
1106
|
candidatesByHash: new Map(),
|
|
1105
1107
|
matched: new Set(),
|
|
1106
1108
|
unmatched: null,
|
|
1107
|
-
unmatchedIdentities: null,
|
|
1108
1109
|
memory: new Map()
|
|
1109
1110
|
}
|
|
1110
1111
|
}
|
|
1111
1112
|
if (entry.kind === "changed") {
|
|
1112
1113
|
const candidate = entry
|
|
1113
1114
|
this.checkpoint(active)
|
|
1114
|
-
const
|
|
1115
|
+
const verified = await this.verifiedAutomaticHash(
|
|
1115
1116
|
active, candidate, group.memory, verifiedHashes, counts)
|
|
1116
|
-
if (!
|
|
1117
|
+
if (!verified) continue
|
|
1118
|
+
const hash = verified.hash
|
|
1117
1119
|
let value = group.candidatesByHash.get(hash)
|
|
1118
1120
|
if (!value) {
|
|
1119
1121
|
value = { candidates: [], identities: new Set() }
|
|
1120
1122
|
group.candidatesByHash.set(hash, value)
|
|
1121
1123
|
}
|
|
1122
|
-
value.candidates.push(
|
|
1123
|
-
value.identities.add(this.automaticIdentityKey(
|
|
1124
|
+
value.candidates.push(verified)
|
|
1125
|
+
value.identities.add(this.automaticIdentityKey(verified))
|
|
1124
1126
|
continue
|
|
1125
1127
|
}
|
|
1126
1128
|
preparePeers()
|
|
1127
1129
|
if (!group.unmatched.size) continue
|
|
1128
1130
|
this.checkpoint(active)
|
|
1129
|
-
const
|
|
1130
|
-
if (group.unmatchedIdentities.size === 1 &&
|
|
1131
|
-
group.unmatchedIdentities.has(identity)) continue
|
|
1132
|
-
const hash = await this.verifiedAutomaticHash(
|
|
1131
|
+
const verified = await this.verifiedAutomaticHash(
|
|
1133
1132
|
active, entry, group.memory, verifiedHashes, counts)
|
|
1134
|
-
const wanted =
|
|
1133
|
+
const wanted = verified && group.unmatched.get(verified.hash)
|
|
1134
|
+
const identity = verified && this.automaticIdentityKey(verified)
|
|
1135
1135
|
if (!wanted || wanted.identity === identity) {
|
|
1136
1136
|
continue
|
|
1137
1137
|
}
|
|
1138
1138
|
for (const candidate of wanted.candidates) {
|
|
1139
1139
|
group.matched.add(pathKey(candidate.path))
|
|
1140
1140
|
}
|
|
1141
|
-
group.unmatched.delete(hash)
|
|
1142
|
-
const remaining = group.unmatchedIdentities.get(wanted.identity) - 1
|
|
1143
|
-
if (remaining) {
|
|
1144
|
-
group.unmatchedIdentities.set(wanted.identity, remaining)
|
|
1145
|
-
} else {
|
|
1146
|
-
group.unmatchedIdentities.delete(wanted.identity)
|
|
1147
|
-
}
|
|
1141
|
+
group.unmatched.delete(verified.hash)
|
|
1148
1142
|
}
|
|
1149
1143
|
if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
|
|
1150
1144
|
await this.vault.registry.rememberHashCache(
|
package/package.json
CHANGED
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
tab.classList.remove("app-vault-result-attention")
|
|
168
168
|
}
|
|
169
169
|
})
|
|
170
|
-
tab.addEventListener("click", () => {
|
|
170
|
+
tab.addEventListener("click", (event) => {
|
|
171
|
+
if (!event.isTrusted) return
|
|
171
172
|
const signature = status.dataset.resultSignature || ""
|
|
172
173
|
if (!signature) return
|
|
173
174
|
requestAutomaticScanFocus(signature)
|
|
@@ -221,7 +221,7 @@ test("the app workspace presents the automatic-result coachmark without starting
|
|
|
221
221
|
assert.doesNotMatch(present, /innerHTML|post\(|btn-scan\.click/)
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
test("selecting a badged row acknowledges it without delaying navigation", async () => {
|
|
224
|
+
test("user-selecting a badged row acknowledges it without delaying navigation", async () => {
|
|
225
225
|
const script = await fs.promises.readFile(
|
|
226
226
|
path.join(root, "server", "public", "app-vault-mode.js"), "utf8")
|
|
227
227
|
const parent = new JSDOM("", { url: "http://localhost/" })
|
|
@@ -256,14 +256,19 @@ test("selecting a badged row acknowledges it without delaying navigation", async
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
dom.window.eval(script)
|
|
260
259
|
const tab = dom.window.document.getElementById("save-space-tab")
|
|
261
|
-
let
|
|
262
|
-
tab.addEventListener(
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
let clickHandler
|
|
261
|
+
const addEventListener = tab.addEventListener.bind(tab)
|
|
262
|
+
tab.addEventListener = (type, listener, options) => {
|
|
263
|
+
if (type === "click") clickHandler = listener
|
|
264
|
+
addEventListener(type, listener, options)
|
|
265
|
+
}
|
|
266
|
+
dom.window.eval(script)
|
|
267
|
+
let preventedByHandler = false
|
|
268
|
+
clickHandler({
|
|
269
|
+
isTrusted: true,
|
|
270
|
+
preventDefault: () => { preventedByHandler = true }
|
|
265
271
|
})
|
|
266
|
-
tab.click()
|
|
267
272
|
|
|
268
273
|
await waitFor(() => requests.length === 1)
|
|
269
274
|
assert.equal(preventedByHandler, false)
|
|
@@ -289,6 +294,49 @@ test("selecting a badged row acknowledges it without delaying navigation", async
|
|
|
289
294
|
parent.window.close()
|
|
290
295
|
})
|
|
291
296
|
|
|
297
|
+
test("programmatic tab clicks do not acknowledge a result", async () => {
|
|
298
|
+
const script = await fs.promises.readFile(
|
|
299
|
+
path.join(root, "server", "public", "app-vault-mode.js"), "utf8")
|
|
300
|
+
const signature = "e".repeat(64)
|
|
301
|
+
const parent = new JSDOM("", { url: "http://localhost/" })
|
|
302
|
+
const dom = new JSDOM(`<a id="save-space-tab" href="/vault/app/ComfyUI">
|
|
303
|
+
<span data-app-vault-result-badge></span>
|
|
304
|
+
<span data-app-vault-mode data-app="ComfyUI" data-mode="automatic"
|
|
305
|
+
data-ready="true" data-result-signature="${signature}">
|
|
306
|
+
<span data-app-vault-mode-label>Auto</span>
|
|
307
|
+
</span>
|
|
308
|
+
</a>`, {
|
|
309
|
+
runScripts: "outside-only",
|
|
310
|
+
url: "http://localhost/v/ComfyUI"
|
|
311
|
+
})
|
|
312
|
+
Object.defineProperty(dom.window, "parent", {
|
|
313
|
+
configurable: true,
|
|
314
|
+
value: parent.window
|
|
315
|
+
})
|
|
316
|
+
parent.window.postMessage = () => {}
|
|
317
|
+
const requests = []
|
|
318
|
+
dom.window.fetch = async (url, options = {}) => {
|
|
319
|
+
requests.push({ url, options })
|
|
320
|
+
return { ok: true, json: async () => ({}) }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dom.window.eval(script)
|
|
324
|
+
const tab = dom.window.document.getElementById("save-space-tab")
|
|
325
|
+
tab.addEventListener("click", (event) => event.preventDefault())
|
|
326
|
+
tab.click()
|
|
327
|
+
|
|
328
|
+
assert.equal(requests.length, 0)
|
|
329
|
+
assert.equal(tab.querySelector("[data-app-vault-result-badge]").hidden,
|
|
330
|
+
false)
|
|
331
|
+
assert.equal(dom.window.document.querySelector("[data-app-vault-mode]")
|
|
332
|
+
.dataset.resultSignature, signature)
|
|
333
|
+
assert.equal(dom.window.sessionStorage.getItem(
|
|
334
|
+
"pinokio:vault:auto-scan-focus:ComfyUI"), null)
|
|
335
|
+
|
|
336
|
+
dom.window.close()
|
|
337
|
+
parent.window.close()
|
|
338
|
+
})
|
|
339
|
+
|
|
292
340
|
test("a retained Disk Saver handoff targets the visible existing frame", async () => {
|
|
293
341
|
const template = await fs.promises.readFile(
|
|
294
342
|
path.join(root, "server", "views", "app.ejs"), "utf8")
|
|
@@ -336,8 +384,15 @@ test("a retained Disk Saver handoff targets the visible existing frame", async (
|
|
|
336
384
|
json: async () => ({ acknowledged: true, app: "ComfyUI" })
|
|
337
385
|
})
|
|
338
386
|
|
|
387
|
+
const tab = dom.window.document.getElementById("save-space-tab")
|
|
388
|
+
let clickHandler
|
|
389
|
+
const addEventListener = tab.addEventListener.bind(tab)
|
|
390
|
+
tab.addEventListener = (type, listener, options) => {
|
|
391
|
+
if (type === "click") clickHandler = listener
|
|
392
|
+
addEventListener(type, listener, options)
|
|
393
|
+
}
|
|
339
394
|
dom.window.eval(script)
|
|
340
|
-
|
|
395
|
+
clickHandler({ isTrusted: true })
|
|
341
396
|
await waitFor(() => visibleMessages.length === 1)
|
|
342
397
|
|
|
343
398
|
assert.deepEqual(hiddenMessages, [])
|
|
@@ -265,6 +265,46 @@ describe("automatic app checks", () => {
|
|
|
265
265
|
await close(vault)
|
|
266
266
|
})
|
|
267
267
|
|
|
268
|
+
test("a watcher batch retains a path recreated after deletion", async () => {
|
|
269
|
+
const home = await makeHome()
|
|
270
|
+
const app = "delete-create-app"
|
|
271
|
+
const root = path.join(home, "api", app)
|
|
272
|
+
const temporary = path.join(root, "temporary")
|
|
273
|
+
const recreated = path.join(temporary, "model.bin")
|
|
274
|
+
const automatic = makeAutomatic({
|
|
275
|
+
enabled: true,
|
|
276
|
+
kernel: { homedir: home, platform: "darwin" }
|
|
277
|
+
})
|
|
278
|
+
automatic.observedApps.add(app)
|
|
279
|
+
|
|
280
|
+
automatic.handleWatcherEvents(null, [
|
|
281
|
+
{ type: "delete", path: temporary },
|
|
282
|
+
{ type: "create", path: recreated }
|
|
283
|
+
])
|
|
284
|
+
|
|
285
|
+
assert.deepEqual([...automatic.changedPaths.get(app)], [recreated])
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test("a watcher batch removes a path deleted after creation", async () => {
|
|
289
|
+
const home = await makeHome()
|
|
290
|
+
const app = "create-delete-app"
|
|
291
|
+
const root = path.join(home, "api", app)
|
|
292
|
+
const temporary = path.join(root, "temporary")
|
|
293
|
+
const removed = path.join(temporary, "model.bin")
|
|
294
|
+
const automatic = makeAutomatic({
|
|
295
|
+
enabled: true,
|
|
296
|
+
kernel: { homedir: home, platform: "darwin" }
|
|
297
|
+
})
|
|
298
|
+
automatic.observedApps.add(app)
|
|
299
|
+
|
|
300
|
+
automatic.handleWatcherEvents(null, [
|
|
301
|
+
{ type: "create", path: removed },
|
|
302
|
+
{ type: "delete", path: temporary }
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
assert.equal(automatic.changedPaths.has(app), false)
|
|
306
|
+
})
|
|
307
|
+
|
|
268
308
|
test("disposing automatic checks releases the watcher and pending work", async () => {
|
|
269
309
|
const home = await makeHome()
|
|
270
310
|
const appRoot = path.join(home, "api", "disposed-app")
|
|
@@ -1428,6 +1468,80 @@ describe("automatic app checks", () => {
|
|
|
1428
1468
|
await close(vault)
|
|
1429
1469
|
})
|
|
1430
1470
|
|
|
1471
|
+
test("files that become hardlinks after staging are not duplicates", async () => {
|
|
1472
|
+
const home = await makeHome()
|
|
1473
|
+
const app = "became-hardlinks-app"
|
|
1474
|
+
const appRoot = path.join(home, "api", app)
|
|
1475
|
+
const first = path.join(appRoot, "first.bin")
|
|
1476
|
+
const second = path.join(appRoot, "second.bin")
|
|
1477
|
+
await fs.promises.mkdir(appRoot)
|
|
1478
|
+
await fs.promises.writeFile(first, "same-content")
|
|
1479
|
+
await fs.promises.writeFile(second, "same-content")
|
|
1480
|
+
const before = await Promise.all([
|
|
1481
|
+
fs.promises.lstat(first),
|
|
1482
|
+
fs.promises.lstat(second)
|
|
1483
|
+
])
|
|
1484
|
+
assert.notEqual(before[0].ino, before[1].ino)
|
|
1485
|
+
const vault = await makeVault(home)
|
|
1486
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1487
|
+
const stage = vault.registry.stageAutomaticPrecheckFiles.bind(
|
|
1488
|
+
vault.registry)
|
|
1489
|
+
vault.registry.stageAutomaticPrecheckFiles = async (...args) => {
|
|
1490
|
+
await stage(...args)
|
|
1491
|
+
await fs.promises.unlink(second)
|
|
1492
|
+
await fs.promises.link(first, second)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
vault.automaticScans.queueApp(app, [first, second])
|
|
1496
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1497
|
+
!vault.automaticScans.entries.has(app))
|
|
1498
|
+
|
|
1499
|
+
const after = await Promise.all([
|
|
1500
|
+
fs.promises.lstat(first),
|
|
1501
|
+
fs.promises.lstat(second)
|
|
1502
|
+
])
|
|
1503
|
+
assert.equal(after[0].ino, after[1].ino)
|
|
1504
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
1505
|
+
await close(vault)
|
|
1506
|
+
})
|
|
1507
|
+
|
|
1508
|
+
test("hardlinks that become independent after staging are duplicates", async () => {
|
|
1509
|
+
const home = await makeHome()
|
|
1510
|
+
const app = "split-hardlinks-app"
|
|
1511
|
+
const appRoot = path.join(home, "api", app)
|
|
1512
|
+
const first = path.join(appRoot, "first.bin")
|
|
1513
|
+
const second = path.join(appRoot, "second.bin")
|
|
1514
|
+
await fs.promises.mkdir(appRoot)
|
|
1515
|
+
await fs.promises.writeFile(first, "same-content")
|
|
1516
|
+
await fs.promises.link(first, second)
|
|
1517
|
+
const before = await Promise.all([
|
|
1518
|
+
fs.promises.lstat(first),
|
|
1519
|
+
fs.promises.lstat(second)
|
|
1520
|
+
])
|
|
1521
|
+
assert.equal(before[0].ino, before[1].ino)
|
|
1522
|
+
const vault = await makeVault(home)
|
|
1523
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1524
|
+
const stage = vault.registry.stageAutomaticPrecheckFiles.bind(
|
|
1525
|
+
vault.registry)
|
|
1526
|
+
vault.registry.stageAutomaticPrecheckFiles = async (...args) => {
|
|
1527
|
+
await stage(...args)
|
|
1528
|
+
await fs.promises.unlink(second)
|
|
1529
|
+
await fs.promises.writeFile(second, "same-content")
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
vault.automaticScans.queueApp(app, [first, second])
|
|
1533
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1534
|
+
vault.automaticScans.entries.get(app)?.state === "result")
|
|
1535
|
+
|
|
1536
|
+
const after = await Promise.all([
|
|
1537
|
+
fs.promises.lstat(first),
|
|
1538
|
+
fs.promises.lstat(second)
|
|
1539
|
+
])
|
|
1540
|
+
assert.notEqual(after[0].ino, after[1].ino)
|
|
1541
|
+
assert.equal(vault.automaticScans.snapshot().rows[0].state, "result")
|
|
1542
|
+
await close(vault)
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1431
1545
|
test("stale file rows and known identical inodes are not peers", async () => {
|
|
1432
1546
|
const home = await makeHome()
|
|
1433
1547
|
const appRoot = path.join(home, "api", "no-peer-app")
|