pinokiod 8.0.76 → 8.0.78

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.
@@ -32,6 +32,12 @@ const pathKey = (value) => {
32
32
  return process.platform === "win32" ? resolved.toLowerCase() : resolved
33
33
  }
34
34
 
35
+ const metadataKey = (entry) => [
36
+ Number(entry.mode) & 0o7777,
37
+ Number(entry.uid),
38
+ Number(entry.gid)
39
+ ].join(":")
40
+
35
41
  class AutomaticScans {
36
42
  constructor(vault) {
37
43
  this.vault = vault
@@ -270,7 +276,7 @@ class AutomaticScans {
270
276
  if (!collected.size) this.discardChangedPaths(app)
271
277
  }
272
278
 
273
- async candidateThreshold() {
279
+ async candidatePolicy() {
274
280
  const cached = this.vault.lastScanCache &&
275
281
  typeof this.vault.lastScanCache.get === "function"
276
282
  ? this.vault.lastScanCache.get("")
@@ -279,9 +285,24 @@ class AutomaticScans {
279
285
  ? await this.vault.registry.scanFor()
280
286
  : null)
281
287
  const threshold = scan ? Number(scan.candidate_min_bytes) : NaN
282
- return Number.isFinite(threshold) && threshold >= 0
283
- ? threshold
284
- : SIZE_THRESHOLD
288
+ const hasLinkableDevices = !!(
289
+ scan && Array.isArray(scan.linkable_devices))
290
+ return {
291
+ threshold: Number.isFinite(threshold) && threshold >= 0
292
+ ? threshold
293
+ : SIZE_THRESHOLD,
294
+ linkability_known: hasLinkableDevices,
295
+ linkable_devices: new Set(hasLinkableDevices
296
+ ? scan.linkable_devices
297
+ .map((dev) => Number(dev))
298
+ .filter((dev) => Number.isFinite(dev))
299
+ : [])
300
+ }
301
+ }
302
+
303
+ async candidateThreshold(policy = null) {
304
+ const current = policy || await this.candidatePolicy()
305
+ return current.threshold
285
306
  }
286
307
 
287
308
  statChangedPaths(paths, options, onSettled = null) {
@@ -910,7 +931,8 @@ class AutomaticScans {
910
931
  }
911
932
  this.checkpoint(active)
912
933
  if (!current.isFile() || current.isSymbolicLink() ||
913
- current.dev !== entry.dev || current.size !== entry.size) {
934
+ current.dev !== entry.dev || current.size !== entry.size ||
935
+ metadataKey(current) !== metadataKey(entry)) {
914
936
  this.logVerificationSkip(
915
937
  active.app, entry, "snapshot-changed", counts)
916
938
  return null
@@ -1016,8 +1038,10 @@ class AutomaticScans {
1016
1038
  hash_failures: 0,
1017
1039
  unstable_hashes: 0
1018
1040
  }
1019
- markStage("threshold-read")
1020
- const threshold = await this.candidateThreshold()
1041
+ markStage("policy-read")
1042
+ const policy = await this.candidatePolicy()
1043
+ const threshold = await this.candidateThreshold(policy)
1044
+ const linkableDevices = policy.linkable_devices
1021
1045
  const paths = [...new Set(active.paths || [])].filter((filePath) =>
1022
1046
  typeof filePath === "string" && inside(root, filePath))
1023
1047
  const verifiedHashes = new Map()
@@ -1025,8 +1049,10 @@ class AutomaticScans {
1025
1049
  app,
1026
1050
  root,
1027
1051
  threshold_bytes: threshold,
1052
+ linkability_known: policy.linkability_known,
1053
+ linkable_devices: [...linkableDevices],
1028
1054
  changed_paths: paths.length,
1029
- policy: "metadata-prefilter-sha256"
1055
+ policy: "eligibility-metadata-prefilter-sha256"
1030
1056
  })
1031
1057
  markStage("staging-reset")
1032
1058
  await this.vault.registry.beginAutomaticPrecheck(app)
@@ -1065,7 +1091,9 @@ class AutomaticScans {
1065
1091
  if (!stat || !stat.isFile() || stat.isSymbolicLink()) continue
1066
1092
  counts.files += 1
1067
1093
  counts.bytes += Math.max(0, Number(stat.size) || 0)
1068
- if (!isCandidateFileSize(stat.size, threshold)) continue
1094
+ if (!isCandidateFileSize(stat.size, threshold) ||
1095
+ (policy.linkability_known &&
1096
+ !linkableDevices.has(stat.dev))) continue
1069
1097
  candidates.push({
1070
1098
  path: paths[index],
1071
1099
  size: stat.size,
@@ -1119,8 +1147,8 @@ class AutomaticScans {
1119
1147
  const preparePeers = () => {
1120
1148
  if (!group || group.unmatched) return
1121
1149
  group.unmatched = new Map()
1122
- for (const [hash, value] of group.candidatesByHash) {
1123
- group.unmatched.set(hash, {
1150
+ for (const [match, value] of group.candidatesByMatch) {
1151
+ group.unmatched.set(match, {
1124
1152
  identity: value.identities.values().next().value,
1125
1153
  candidate: value.candidate
1126
1154
  })
@@ -1149,10 +1177,11 @@ class AutomaticScans {
1149
1177
  )
1150
1178
  if (!verified) continue
1151
1179
  const hash = verified.hash
1152
- let value = group.candidatesByHash.get(hash)
1180
+ const match = `${hash}\0${metadataKey(verified)}`
1181
+ let value = group.candidatesByMatch.get(match)
1153
1182
  if (!value) {
1154
1183
  value = { candidate: verified, identities: new Set() }
1155
- group.candidatesByHash.set(hash, value)
1184
+ group.candidatesByMatch.set(match, value)
1156
1185
  }
1157
1186
  value.identities.add(this.automaticIdentityKey(verified))
1158
1187
  if (value.identities.size > 1) {
@@ -1197,7 +1226,7 @@ class AutomaticScans {
1197
1226
  key,
1198
1227
  candidates: [],
1199
1228
  candidatesHashed: false,
1200
- candidatesByHash: new Map(),
1229
+ candidatesByMatch: new Map(),
1201
1230
  unmatched: null,
1202
1231
  memory: new Map()
1203
1232
  }
@@ -1222,13 +1251,17 @@ class AutomaticScans {
1222
1251
  if (!await hashCandidates(identity)) continue
1223
1252
  if (proof) return await complete()
1224
1253
  if (!group.unmatched.size) continue
1254
+ const peerMetadata = metadataKey(peer.entry)
1225
1255
  if (![...group.unmatched.values()].some((wanted) =>
1226
- wanted.identity !== identity)) continue
1256
+ wanted.identity !== identity &&
1257
+ metadataKey(wanted.candidate) === peerMetadata)) continue
1227
1258
  this.checkpoint(active)
1228
1259
  markStage("peer-hash", entry)
1229
1260
  const verified = await this.verifiedAutomaticHash(
1230
1261
  active, entry, group.memory, verifiedHashes, counts, peer)
1231
- const wanted = verified && group.unmatched.get(verified.hash)
1262
+ const match = verified &&
1263
+ `${verified.hash}\0${metadataKey(verified)}`
1264
+ const wanted = match && group.unmatched.get(match)
1232
1265
  if (!wanted || wanted.identity === identity) {
1233
1266
  continue
1234
1267
  }
@@ -2807,6 +2807,9 @@ class RegistryCore {
2807
2807
  WHERE peer.app = candidate.app
2808
2808
  AND peer.dev = candidate.dev
2809
2809
  AND peer.size = candidate.size
2810
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
2811
+ AND peer.uid = candidate.uid
2812
+ AND peer.gid = candidate.gid
2810
2813
  AND pinokio_path_key(peer.path) !=
2811
2814
  pinokio_path_key(candidate.path)
2812
2815
  )
@@ -2814,7 +2817,10 @@ class RegistryCore {
2814
2817
  SELECT 1 FROM files peer
2815
2818
  WHERE peer.dev = candidate.dev
2816
2819
  AND peer.size = candidate.size
2817
- AND peer.unavailable_reason IS NOT 'stale'
2820
+ AND peer.unavailable_reason IS NULL
2821
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
2822
+ AND peer.uid = candidate.uid
2823
+ AND peer.gid = candidate.gid
2818
2824
  AND pinokio_path_key(peer.path) !=
2819
2825
  pinokio_path_key(candidate.path)
2820
2826
  )
@@ -2823,6 +2829,9 @@ class RegistryCore {
2823
2829
  WHERE peer.verified_at IS NOT NULL
2824
2830
  AND peer.dev = candidate.dev
2825
2831
  AND peer.size = candidate.size
2832
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
2833
+ AND peer.uid = candidate.uid
2834
+ AND peer.gid = candidate.gid
2826
2835
  AND pinokio_path_key(peer.path) !=
2827
2836
  pinokio_path_key(candidate.path)
2828
2837
  )
@@ -2888,6 +2897,7 @@ class RegistryCore {
2888
2897
  WHERE candidate.app = @app
2889
2898
  AND candidate.dev = @dev
2890
2899
  AND candidate.size = @size
2900
+ AND ${possiblePeer}
2891
2901
  AND candidate.path > @path
2892
2902
  ORDER BY candidate.path
2893
2903
  LIMIT @limit
@@ -2948,7 +2958,7 @@ class RegistryCore {
2948
2958
  WHERE peer.dev = @dev
2949
2959
  AND peer.size = @size
2950
2960
  AND peer.path > @path
2951
- AND peer.unavailable_reason IS NOT 'stale'
2961
+ AND peer.unavailable_reason IS NULL
2952
2962
  ORDER BY peer.path
2953
2963
  LIMIT @limit
2954
2964
  `).all({ dev, size, path: afterPath, limit: pageSize })
@@ -3030,7 +3040,7 @@ class RegistryCore {
3030
3040
  hashes.set(canonicalPathKey(entry.path), entry.hash)
3031
3041
  }
3032
3042
  const rows = this.database.prepare(`
3033
- SELECT path, dev, ino, size, mtime, ctime
3043
+ SELECT path, dev, ino, size, mtime, ctime, mode, uid, gid
3034
3044
  FROM automatic_precheck_files
3035
3045
  WHERE app = ?
3036
3046
  ORDER BY pinokio_path_key(path), path, dev, ino, size, mtime, ctime
@@ -3048,7 +3058,10 @@ class RegistryCore {
3048
3058
  row.ino,
3049
3059
  row.size,
3050
3060
  row.mtime,
3051
- row.ctime
3061
+ row.ctime,
3062
+ Number(row.mode) & 0o7777,
3063
+ row.uid,
3064
+ row.gid
3052
3065
  ]) {
3053
3066
  digest.update(String(value))
3054
3067
  digest.update("\0")
@@ -3062,6 +3075,12 @@ class RegistryCore {
3062
3075
  digest.update("\0")
3063
3076
  digest.update(String(proof.row.size))
3064
3077
  digest.update("\0")
3078
+ digest.update(String(Number(proof.row.mode) & 0o7777))
3079
+ digest.update("\0")
3080
+ digest.update(String(proof.row.uid))
3081
+ digest.update("\0")
3082
+ digest.update(String(proof.row.gid))
3083
+ digest.update("\0")
3065
3084
  digest.update(proof.hash)
3066
3085
  return {
3067
3086
  signature: digest.digest("hex"),
@@ -155,7 +155,6 @@ class Sweeper {
155
155
  : "complete"
156
156
  this.state.phase = "publishing"
157
157
  this.state.duration_ms = Date.now() - this.state.started
158
- const metadata = this.scanMetadata(scopeId, outcome)
159
158
  const stores = anchorStores
160
159
  .filter((store) =>
161
160
  store.available && Number.isFinite(store.dev))
@@ -165,6 +164,12 @@ class Sweeper {
165
164
  can_link: store.mode !== "copy",
166
165
  root: store.root
167
166
  }))
167
+ const metadata = this.scanMetadata(scopeId, outcome)
168
+ if (!scopeId) {
169
+ metadata.linkable_devices = [...new Set(stores
170
+ .filter((store) => store.can_link)
171
+ .map((store) => store.dev))].sort((left, right) => left - right)
172
+ }
168
173
  const publication = await registry.publishScan(
169
174
  runId,
170
175
  this.publicationSourceIds(scopeId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.76",
3
+ "version": "8.0.78",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -2133,7 +2133,11 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
2133
2133
  .vault-event-time { color: var(--task-muted); font-size: 10.5px; }
2134
2134
  .vault-unavailable { color: var(--task-muted); font-size: 11px; }
2135
2135
  @media (max-width: 1140px) {
2136
- .vault-overview { align-items: flex-start; flex-direction: column; }
2136
+ .vault-overview {
2137
+ min-height: auto;
2138
+ align-items: flex-start;
2139
+ flex-direction: column;
2140
+ }
2137
2141
  .vault-overview-actions { width: 100%; justify-content: flex-start; }
2138
2142
  .vault-scan-coachmark { right: auto; left: 0; }
2139
2143
  .vault-metrics { width: 100%; }
@@ -48,6 +48,13 @@ const makeVault = async (home, options = {}) => {
48
48
  : true
49
49
  vault.globalScanReady = async () => globalScanReady
50
50
  vault.automaticScans.globalScanReady = globalScanReady
51
+ if (globalScanReady) {
52
+ const homeStat = await fs.promises.stat(home)
53
+ vault.lastScanCache.set("", {
54
+ candidate_min_bytes: SIZE_THRESHOLD,
55
+ linkable_devices: [homeStat.dev]
56
+ })
57
+ }
51
58
  return vault
52
59
  }
53
60
 
@@ -1037,7 +1044,10 @@ describe("automatic app checks", () => {
1037
1044
  ["ino", 22],
1038
1045
  ["size", 201],
1039
1046
  ["mtime", 203],
1040
- ["ctime", 204]
1047
+ ["ctime", 204],
1048
+ ["mode", 0o640],
1049
+ ["uid", 1],
1050
+ ["gid", 1]
1041
1051
  ]) {
1042
1052
  assert.notEqual(
1043
1053
  signature([proof, Object.assign({}, unmatched, { [field]: value })]),
@@ -1161,6 +1171,103 @@ describe("automatic app checks", () => {
1161
1171
  await close(vault)
1162
1172
  })
1163
1173
 
1174
+ test("metadata-incompatible copies do not publish a result", async (t) => {
1175
+ if (process.platform === "win32") {
1176
+ t.skip("POSIX permission metadata is required")
1177
+ return
1178
+ }
1179
+ const home = await makeHome()
1180
+ const app = "metadata-incompatible-app"
1181
+ const appRoot = path.join(home, "api", app)
1182
+ const first = path.join(appRoot, "first.bin")
1183
+ const second = path.join(appRoot, "second.bin")
1184
+ await fs.promises.mkdir(appRoot)
1185
+ await fs.promises.writeFile(first, "same-content")
1186
+ await fs.promises.writeFile(second, "same-content")
1187
+ await fs.promises.chmod(first, 0o600)
1188
+ await fs.promises.chmod(second, 0o644)
1189
+ const vault = await makeVault(home)
1190
+ vault.automaticScans.candidateThreshold = async () => 1
1191
+ const hashedPaths = []
1192
+ const originalHashFile = vault.hashFile.bind(vault)
1193
+ vault.hashFile = async (filePath, options) => {
1194
+ hashedPaths.push(filePath)
1195
+ return originalHashFile(filePath, options)
1196
+ }
1197
+
1198
+ vault.automaticScans.queueApp(app, [first, second])
1199
+ await waitFor(() => !vault.automaticScans.active &&
1200
+ !vault.automaticScans.entries.has(app))
1201
+
1202
+ assert.deepEqual(hashedPaths, [])
1203
+ assert.deepEqual(vault.automaticScans.snapshot().rows, [])
1204
+ await close(vault)
1205
+ })
1206
+
1207
+ test("devices without hardlink support do not publish a result", async () => {
1208
+ const home = await makeHome()
1209
+ const app = "copy-only-device-app"
1210
+ const appRoot = path.join(home, "api", app)
1211
+ const first = path.join(appRoot, "first.bin")
1212
+ const second = path.join(appRoot, "second.bin")
1213
+ await fs.promises.mkdir(appRoot)
1214
+ await fs.promises.writeFile(first, "same-content")
1215
+ await fs.promises.writeFile(second, "same-content")
1216
+ const vault = await makeVault(home)
1217
+ vault.automaticScans.candidateThreshold = async () => 1
1218
+ vault.automaticScans.candidatePolicy = async () => ({
1219
+ threshold: 1,
1220
+ linkability_known: true,
1221
+ linkable_devices: new Set()
1222
+ })
1223
+ const hashedPaths = []
1224
+ const originalHashFile = vault.hashFile.bind(vault)
1225
+ vault.hashFile = async (filePath, options) => {
1226
+ hashedPaths.push(filePath)
1227
+ return originalHashFile(filePath, options)
1228
+ }
1229
+
1230
+ vault.automaticScans.queueApp(app, [first, second])
1231
+ await waitFor(() => !vault.automaticScans.active &&
1232
+ !vault.automaticScans.entries.has(app))
1233
+
1234
+ assert.deepEqual(hashedPaths, [])
1235
+ assert.deepEqual(vault.automaticScans.snapshot().rows, [])
1236
+ await close(vault)
1237
+ })
1238
+
1239
+ test("legacy global scans without linkability metadata preserve automatic checks", async () => {
1240
+ const home = await makeHome()
1241
+ const app = "legacy-global-scan-app"
1242
+ const appRoot = path.join(home, "api", app)
1243
+ const first = path.join(appRoot, "first.bin")
1244
+ const second = path.join(appRoot, "second.bin")
1245
+ await fs.promises.mkdir(appRoot)
1246
+ await fs.promises.writeFile(first, "same-content")
1247
+ await fs.promises.writeFile(second, "same-content")
1248
+ const vault = await makeVault(home)
1249
+ vault.lastScanCache.set("", { candidate_min_bytes: 1 })
1250
+ const hashedPaths = []
1251
+ const originalHashFile = vault.hashFile.bind(vault)
1252
+ vault.hashFile = async (filePath, options) => {
1253
+ hashedPaths.push(filePath)
1254
+ return originalHashFile(filePath, options)
1255
+ }
1256
+
1257
+ vault.automaticScans.queueApp(app, [first, second])
1258
+ await waitFor(() => {
1259
+ const row = vault.automaticScans.entries.get(app)
1260
+ return !vault.automaticScans.active && row && row.state === "result"
1261
+ })
1262
+
1263
+ assert.deepEqual(hashedPaths.sort(), [first, second].sort())
1264
+ assert.deepEqual(vault.automaticScans.snapshot().rows.map((row) => ({
1265
+ app: row.app,
1266
+ state: row.state
1267
+ })), [{ app, state: "result" }])
1268
+ await close(vault)
1269
+ })
1270
+
1164
1271
  test("hash worker cancellation interrupts a read without poisoning the worker", async () => {
1165
1272
  const home = await makeHome()
1166
1273
  const large = path.join(home, "large.bin")
@@ -2113,7 +2220,7 @@ describe("automatic app checks", () => {
2113
2220
  await close(vault)
2114
2221
  })
2115
2222
 
2116
- test("peer verification rejects changed type, device, and size", async (t) => {
2223
+ test("peer verification rejects changed eligibility metadata", async (t) => {
2117
2224
  const home = await makeHome()
2118
2225
  const automatic = makeAutomatic({
2119
2226
  enabled: true,
@@ -2141,6 +2248,18 @@ describe("automatic app checks", () => {
2141
2248
  {
2142
2249
  name: "size",
2143
2250
  stat: { file: true, link: false, dev: 1, size: 5 }
2251
+ },
2252
+ {
2253
+ name: "permissions",
2254
+ stat: { file: true, link: false, dev: 1, size: 4, mode: 1 }
2255
+ },
2256
+ {
2257
+ name: "owner",
2258
+ stat: { file: true, link: false, dev: 1, size: 4, uid: 1 }
2259
+ },
2260
+ {
2261
+ name: "group",
2262
+ stat: { file: true, link: false, dev: 1, size: 4, gid: 1 }
2144
2263
  }
2145
2264
  ]
2146
2265
  const currentStats = new Map(cases.map(({ name, stat }) => [
@@ -2154,9 +2273,9 @@ describe("automatic app checks", () => {
2154
2273
  mtimeMs: 1,
2155
2274
  ctimeMs: 1,
2156
2275
  nlink: 1,
2157
- mode: 0,
2158
- uid: 0,
2159
- gid: 0
2276
+ mode: stat.mode || 0,
2277
+ uid: stat.uid || 0,
2278
+ gid: stat.gid || 0
2160
2279
  }
2161
2280
  ]))
2162
2281
  const lstat = fs.promises.lstat
@@ -2172,7 +2291,10 @@ describe("automatic app checks", () => {
2172
2291
  size: 4,
2173
2292
  ino: 1,
2174
2293
  mtime: 1,
2175
- ctime: 1
2294
+ ctime: 1,
2295
+ mode: 0,
2296
+ uid: 0,
2297
+ gid: 0
2176
2298
  }, counts), null)
2177
2299
  }
2178
2300
 
@@ -3426,7 +3548,11 @@ describe("automatic app checks", () => {
3426
3548
  candidate("hardlink-a.bin", 108, 7),
3427
3549
  candidate("hardlink-b.bin", 108, 7),
3428
3550
  candidate("file-peer.bin", 109, 8),
3429
- candidate("anchor-peer.bin", 110, 9)
3551
+ candidate("anchor-peer.bin", 110, 9),
3552
+ candidate("unavailable-peer.bin", 111, 10),
3553
+ Object.assign(candidate("metadata-mismatch.bin", 108, 11), {
3554
+ mode: 1
3555
+ })
3430
3556
  ]
3431
3557
  const file = (filePath, size, dev, ino, unavailableReason = null) => ({
3432
3558
  path: filePath,
@@ -3454,6 +3580,13 @@ describe("automatic app checks", () => {
3454
3580
  path.join(home, "stale-peer.bin"), 106, 1, 23, "stale"))
3455
3581
  await registry.upsertFile(file(
3456
3582
  path.join(home, "valid-file-peer.bin"), 109, 1, 24))
3583
+ const unavailableWithinEligibleGroup = path.join(
3584
+ home, "unavailable-within-eligible-group.bin")
3585
+ await registry.upsertFile(file(
3586
+ unavailableWithinEligibleGroup, 109, 1, 25, "metadata"))
3587
+ await registry.upsertFile(file(
3588
+ path.join(home, "unavailable-file-peer.bin"), 111, 1, 26,
3589
+ "metadata"))
3457
3590
 
3458
3591
  for (const [name, size, verifiedAt] of [
3459
3592
  ["unverified", 107, null],
@@ -3497,6 +3630,9 @@ describe("automatic app checks", () => {
3497
3630
  WHERE peer.app = candidate.app
3498
3631
  AND peer.dev = candidate.dev
3499
3632
  AND peer.size = candidate.size
3633
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
3634
+ AND peer.uid = candidate.uid
3635
+ AND peer.gid = candidate.gid
3500
3636
  AND pinokio_path_key(peer.path) !=
3501
3637
  pinokio_path_key(candidate.path)
3502
3638
  )
@@ -3504,7 +3640,10 @@ describe("automatic app checks", () => {
3504
3640
  SELECT 1 FROM files peer
3505
3641
  WHERE peer.dev = candidate.dev
3506
3642
  AND peer.size = candidate.size
3507
- AND peer.unavailable_reason IS NOT 'stale'
3643
+ AND peer.unavailable_reason IS NULL
3644
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
3645
+ AND peer.uid = candidate.uid
3646
+ AND peer.gid = candidate.gid
3508
3647
  AND pinokio_path_key(peer.path) !=
3509
3648
  pinokio_path_key(candidate.path)
3510
3649
  )
@@ -3513,6 +3652,9 @@ describe("automatic app checks", () => {
3513
3652
  WHERE peer.verified_at IS NOT NULL
3514
3653
  AND peer.dev = candidate.dev
3515
3654
  AND peer.size = candidate.size
3655
+ AND (peer.mode & 4095) = (candidate.mode & 4095)
3656
+ AND peer.uid = candidate.uid
3657
+ AND peer.gid = candidate.gid
3516
3658
  AND pinokio_path_key(peer.path) !=
3517
3659
  pinokio_path_key(candidate.path)
3518
3660
  )
@@ -3525,9 +3667,11 @@ describe("automatic app checks", () => {
3525
3667
  assert.match(plan, /files_size_device_path_idx/)
3526
3668
  assert.match(plan, /anchors_size_device_path_idx/)
3527
3669
  const changed = []
3670
+ const returnedPaths = []
3528
3671
  let cursor = null
3529
3672
  do {
3530
3673
  const page = registry.automaticPrecheckEntries(app, cursor, 2)
3674
+ returnedPaths.push(...page.entries.map((entry) => entry.path))
3531
3675
  changed.push(...page.entries
3532
3676
  .filter((entry) => entry.kind === "changed")
3533
3677
  .map((entry) => entry.path))
@@ -3540,6 +3684,7 @@ describe("automatic app checks", () => {
3540
3684
  entries[8].path,
3541
3685
  entries[9].path
3542
3686
  ].sort())
3687
+ assert.equal(returnedPaths.includes(unavailableWithinEligibleGroup), false)
3543
3688
  registry.close()
3544
3689
  })
3545
3690
 
@@ -189,6 +189,13 @@ describe("Save Space engine", () => {
189
189
 
190
190
  assert.equal((await vault.status()).global_candidate_min_bytes, selected)
191
191
  assert.equal(await vault.automaticScans.candidateThreshold(), selected)
192
+ const scan = await vault.registry.scanFor()
193
+ const expectedDevices = vault.anchorStores()
194
+ .filter((store) => store.available && Number.isFinite(store.dev) &&
195
+ store.mode !== "copy")
196
+ .map((store) => store.dev)
197
+ .sort((left, right) => left - right)
198
+ assert.deepEqual(scan.linkable_devices, expectedDevices)
192
199
 
193
200
  await close(vault)
194
201
  })
@@ -546,6 +546,8 @@ describe("Save Space interface", () => {
546
546
  assert.match(combined, /unique to this app/)
547
547
  assert.match(vaultCss,
548
548
  /body\.vault-page \.vault-view-tabs\s*\{[^}]*padding:\s*0;/s)
549
+ assert.match(vaultCss,
550
+ /@media \(max-width: 1140px\)[\s\S]*?\.vault-overview\s*\{[^}]*min-height:\s*auto;/s)
549
551
  assert.match(vaultCss,
550
552
  /body\.dark\.vault-page\s*\{[^}]*--task-panel:\s*var\(--pinokio-sidebar-tabbar-bg\);/s)
551
553
  assert.match(vaultCss,