diff --git a/src/common/pinstate.h b/src/common/pinstate.h
index 053e7062f..44ee12417 100644
--- a/src/common/pinstate.h
+++ b/src/common/pinstate.h
@@ -73,6 +73,47 @@ enum class PinState {
     Unspecified = 3,
 };
 
+/** A user-facing version of PinState.
+ *
+ * PinStates communicate availability intent for an item, but particular
+ * situations can get complex: An AlwaysLocal folder can have OnlineOnly
+ * files or directories.
+ *
+ * For users this is condensed to a few useful cases.
+ *
+ * Note that this is only about *intent*. The file could still be out of date,
+ * or not have been synced for other reasons, like errors.
+ */
+enum class VfsItemAvailability {
+    /** The item and all its subitems are hydrated and pinned AlwaysLocal.
+     *
+     * This guarantees that all contents will be kept in sync.
+     */
+    AlwaysLocal,
+
+    /** The item and all its subitems are hydrated.
+     *
+     * This may change if the platform or client decide to dehydrate items
+     * that have Unspecified pin state.
+     *
+     * A folder with no file contents will have this availability.
+     */
+    AllHydrated,
+
+    /** There are dehydrated items but the pin state isn't all OnlineOnly.
+     *
+     * This would happen if a dehydration happens to a Unspecified item that
+     * used to be hydrated.
+     */
+    SomeDehydrated,
+
+    /** The item and all its subitems are dehydrated and OnlineOnly.
+     *
+     * This guarantees that contents will not take up space.
+     */
+    OnlineOnly,
+};
+
 }
 
 #endif
diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp
index e613358a6..b22e041ce 100644
--- a/src/common/syncjournaldb.cpp
+++ b/src/common/syncjournaldb.cpp
@@ -1320,6 +1320,31 @@ bool SyncJournalDb::updateLocalMetadata(const QString &filename,
     return _setFileRecordLocalMetadataQuery.exec();
 }
 
+Optional<bool> SyncJournalDb::hasDehydratedFiles(const QByteArray &filename)
+{
+    QMutexLocker locker(&_mutex);
+    if (!checkConnect())
+        return {};
+
+    auto &query = _countDehydratedFilesQuery;
+    static_assert(ItemTypeVirtualFile == 4 && ItemTypeVirtualFileDownload == 5, "");
+    if (!query.initOrReset(QByteArrayLiteral(
+            "SELECT count(*) FROM metadata"
+            " WHERE (" IS_PREFIX_PATH_OR_EQUAL("?1", "path") " OR ?1 == '')"
+            " AND (type == 4 OR type == 5);"), _db)) {
+        return {};
+    }
+
+    query.bindValue(1, filename);
+    if (!query.exec())
+        return {};
+
+    if (!query.next().hasData)
+        return {};
+
+    return query.intValue(0) > 0;
+}
+
 static void toDownloadInfo(SqlQuery &query, SyncJournalDb::DownloadInfo *res)
 {
     bool ok = true;
@@ -2152,7 +2177,7 @@ Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPath(const QByt
             // (it'd be great if paths started with a / and "/" could be the root)
             " (" IS_PREFIX_PATH_OR_EQUAL("path", "?1") " OR path == '')"
             " AND pinState is not null AND pinState != 0"
-            " ORDER BY length(path) DESC;"),
+            " ORDER BY length(path) DESC LIMIT 1;"),
         _db->_db));
     query.bindValue(1, path);
     query.exec();
@@ -2167,6 +2192,43 @@ Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPath(const QByt
     return static_cast<PinState>(query.intValue(0));
 }
 
+Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPathRecursive(const QByteArray &path)
+{
+    // Get the item's effective pin state. We'll compare subitem's pin states
+    // against this.
+    const auto basePin = effectiveForPath(path);
+    if (!basePin)
+        return {};
+
+    QMutexLocker lock(&_db->_mutex);
+    if (!_db->checkConnect())
+        return {};
+
+    // Find all the non-inherited pin states below the item
+    auto &query = _db->_getSubPinsQuery;
+    ASSERT(query.initOrReset(QByteArrayLiteral(
+            "SELECT DISTINCT pinState FROM flags WHERE"
+            " (" IS_PREFIX_PATH_OF("?1", "path") " OR ?1 == '')"
+            " AND pinState is not null and pinState != 0;"),
+        _db->_db));
+    query.bindValue(1, path);
+    query.exec();
+
+    // Check if they are all identical
+    forever {
+        auto next = query.next();
+        if (!next.ok)
+            return {};
+        if (!next.hasData)
+            break;
+        const auto subPin = static_cast<PinState>(query.intValue(0));
+        if (subPin != *basePin)
+            return PinState::Inherited;
+    }
+
+    return *basePin;
+}
+
 void SyncJournalDb::PinStateInterface::setForPath(const QByteArray &path, PinState state)
 {
     QMutexLocker lock(&_db->_mutex);
diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h
index cf0cf61a3..a68960555 100644
--- a/src/common/syncjournaldb.h
+++ b/src/common/syncjournaldb.h
@@ -72,6 +72,10 @@ public:
         const QByteArray &contentChecksumType);
     bool updateLocalMetadata(const QString &filename,
         qint64 modtime, qint64 size, quint64 inode);
+
+    /** Returns whether the item or any subitems are dehydrated */
+    Optional<bool> hasDehydratedFiles(const QByteArray &filename);
+
     bool exists();
     void walCheckpoint();
 
@@ -284,6 +288,21 @@ public:
          */
         Optional<PinState> effectiveForPath(const QByteArray &path);
 
+        /**
+         * Like effectiveForPath() but also considers subitem pin states.
+         *
+         * If the path's pin state and all subitem's pin states are identical
+         * then that pin state will be returned.
+         *
+         * If some subitem's pin state is different from the path's state,
+         * PinState::Inherited will be returned. Inherited isn't returned in
+         * any other cases.
+         *
+         * It's valid to use the root path "".
+         * Returns none on db error.
+         */
+        Optional<PinState> effectiveForPathRecursive(const QByteArray &path);
+
         /**
          * Sets a path's pin state.
          *
@@ -386,6 +405,8 @@ private:
     SqlQuery _deleteConflictRecordQuery;
     SqlQuery _getRawPinStateQuery;
     SqlQuery _getEffectivePinStateQuery;
+    SqlQuery _getSubPinsQuery;
+    SqlQuery _countDehydratedFilesQuery;
     SqlQuery _setPinStateQuery;
     SqlQuery _wipePinStateQuery;
 
diff --git a/src/common/vfs.cpp b/src/common/vfs.cpp
index a40b396a7..5f170daf1 100644
--- a/src/common/vfs.cpp
+++ b/src/common/vfs.cpp
@@ -79,6 +79,27 @@ Optional<PinState> Vfs::pinStateInDb(const QString &folderPath)
     return _setupParams.journal->internalPinStates().effectiveForPath(folderPath.toUtf8());
 }
 
+Optional<VfsItemAvailability> Vfs::availabilityInDb(const QString &folderPath, const QString &pinPath)
+{
+    auto pin = _setupParams.journal->internalPinStates().effectiveForPathRecursive(pinPath.toUtf8());
+    // not being able to retrieve the pin state isn't too bad
+    Optional<bool> hasDehydrated = _setupParams.journal->hasDehydratedFiles(folderPath.toUtf8());
+    if (!hasDehydrated)
+        return {};
+
+    if (*hasDehydrated) {
+        if (pin && *pin == PinState::OnlineOnly)
+            return VfsItemAvailability::OnlineOnly;
+        else
+            return VfsItemAvailability::SomeDehydrated;
+    } else {
+        if (pin && *pin == PinState::AlwaysLocal)
+            return VfsItemAvailability::AlwaysLocal;
+        else
+            return VfsItemAvailability::AllHydrated;
+    }
+}
+
 VfsOff::VfsOff(QObject *parent)
     : Vfs(parent)
 {
@@ -184,3 +205,22 @@ std::unique_ptr<Vfs> OCC::createVfsFromPlugin(Vfs::Mode mode)
     qCInfo(lcPlugin) << "Created VFS instance from plugin" << pluginPath;
     return vfs;
 }
+
+QString OCC::vfsItemAvailabilityToString(VfsItemAvailability availability, bool forFolder)
+{
+    switch(availability) {
+    case VfsItemAvailability::AlwaysLocal:
+        return Vfs::tr("Always available locally");
+    case VfsItemAvailability::AllHydrated:
+        return Vfs::tr("Available locally");
+    case VfsItemAvailability::SomeDehydrated:
+        if (forFolder) {
+            return Vfs::tr("Some available online only");
+        } else {
+            return Vfs::tr("Available online only");
+        }
+    case VfsItemAvailability::OnlineOnly:
+        return Vfs::tr("Available online only");
+    }
+    ENFORCE(false);
+}
diff --git a/src/common/vfs.h b/src/common/vfs.h
index 2ccd3958d..a8765d793 100644
--- a/src/common/vfs.h
+++ b/src/common/vfs.h
@@ -188,11 +188,13 @@ public:
     virtual bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) = 0;
 
     /** Sets the pin state for the item at a path.
+     *
+     * The pin state is set on the item and for all items below it.
      *
      * Usually this would forward to setting the pin state flag in the db table,
      * but some vfs plugins will store the pin state in file attributes instead.
      *
-     * folderPath is relative to the sync folder.
+     * folderPath is relative to the sync folder. Can be "" for root folder.
      */
     virtual bool setPinState(const QString &folderPath, PinState state) = 0;
 
@@ -201,10 +203,19 @@ public:
      * Usually backed by the db's effectivePinState() function but some vfs
      * plugins will override it to retrieve the state from elsewhere.
      *
-     * folderPath is relative to the sync folder.
+     * folderPath is relative to the sync folder. Can be "" for root folder.
      */
     virtual Optional<PinState> pinState(const QString &folderPath) = 0;
 
+    /** Returns availability status of an item at a path.
+     *
+     * The availability is a condensed user-facing version of PinState. See
+     * VfsItemAvailability for details.
+     *
+     * folderPath is relative to the sync folder. Can be "" for root folder.
+     */
+    virtual Optional<VfsItemAvailability> availability(const QString &folderPath) = 0;
+
 public slots:
     /** Update in-sync state based on SyncFileStatusTracker signal.
      *
@@ -235,6 +246,8 @@ protected:
     // Db-backed pin state handling. Derived classes may use it to implement pin states.
     bool setPinStateInDb(const QString &folderPath, PinState state);
     Optional<PinState> pinStateInDb(const QString &folderPath);
+    // sadly for virtual files the path in the metadata table can differ from path in 'flags'
+    Optional<VfsItemAvailability> availabilityInDb(const QString &folderPath, const QString &pinPath);
 
     // the parameters passed to start()
     VfsSetupParams _setupParams;
@@ -269,6 +282,7 @@ public:
 
     bool setPinState(const QString &, PinState) override { return true; }
     Optional<PinState> pinState(const QString &) override { return PinState::AlwaysLocal; }
+    Optional<VfsItemAvailability> availability(const QString &) override { return VfsItemAvailability::AlwaysLocal; }
 
 public slots:
     void fileStatusChanged(const QString &, SyncFileStatus) override {}
@@ -286,4 +300,7 @@ OCSYNC_EXPORT Vfs::Mode bestAvailableVfsMode();
 /// Create a VFS instance for the mode, returns nullptr on failure.
 OCSYNC_EXPORT std::unique_ptr<Vfs> createVfsFromPlugin(Vfs::Mode mode);
 
+/// Convert availability to translated string
+OCSYNC_EXPORT QString vfsItemAvailabilityToString(VfsItemAvailability availability, bool forFolder);
+
 } // namespace OCC
diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp
index 69ade7872..05dd56a4d 100644
--- a/src/gui/accountsettings.cpp
+++ b/src/gui/accountsettings.cpp
@@ -446,14 +446,18 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
 
     if (folder->supportsVirtualFiles()) {
         auto availabilityMenu = menu->addMenu(tr("Availability"));
-        ac = availabilityMenu->addAction(tr("Local"));
-        ac->setCheckable(true);
-        ac->setChecked(!folder->newFilesAreVirtual());
+        auto availability = folder->vfs().availability(QString());
+        if (availability) {
+            ac = availabilityMenu->addAction(vfsItemAvailabilityToString(*availability, true));
+            ac->setEnabled(false);
+        }
+
+        ac = availabilityMenu->addAction(tr("Make always available locally"));
+        ac->setEnabled(!availability || *availability != VfsItemAvailability::AlwaysLocal);
         connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); });
 
-        ac = availabilityMenu->addAction(tr("Online only"));
-        ac->setCheckable(true);
-        ac->setChecked(folder->newFilesAreVirtual());
+        ac = availabilityMenu->addAction(tr("Free up local space"));
+        ac->setEnabled(!availability || *availability != VfsItemAvailability::OnlineOnly);
         connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); });
 
         ac = menu->addAction(tr("Disable virtual file support..."));
diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp
index b96619d40..7dcccee26 100644
--- a/src/gui/socketapi.cpp
+++ b/src/gui/socketapi.cpp
@@ -1042,68 +1042,61 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
     if (syncFolder
         && syncFolder->supportsVirtualFiles()
         && syncFolder->vfs().socketApiPinStateActionsShown()) {
-        bool hasAlwaysLocal = false;
-        bool hasOnlineOnly = false;
-        bool hasHydratedOnlineOnly = false;
-        bool hasDehydratedOnlineOnly = false;
+        ENFORCE(!files.isEmpty());
+
+        // Determine the combined availability status of the files
+        auto combined = Optional<VfsItemAvailability>();
+        auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) {
+            if (lhs == rhs)
+                return lhs;
+            if (lhs == VfsItemAvailability::SomeDehydrated || rhs == VfsItemAvailability::SomeDehydrated
+                || lhs == VfsItemAvailability::OnlineOnly || rhs == VfsItemAvailability::OnlineOnly) {
+                return VfsItemAvailability::SomeDehydrated;
+            }
+            return VfsItemAvailability::AllHydrated;
+        };
+        bool isFolderOrMultiple = false;
         for (const auto &file : files) {
             auto fileData = FileData::get(file);
-            auto path = fileData.folderRelativePathNoVfsSuffix();
-            auto pinState = syncFolder->vfs().pinState(path);
-            if (!pinState) {
-                // db error
-                hasAlwaysLocal = true;
-                hasOnlineOnly = true;
-            } else if (*pinState == PinState::AlwaysLocal) {
-                hasAlwaysLocal = true;
-            } else if (*pinState == PinState::OnlineOnly) {
-                hasOnlineOnly = true;
-                auto record = fileData.journalRecord();
-                if (record._type == ItemTypeFile)
-                    hasHydratedOnlineOnly = true;
-                if (record.isVirtualFile())
-                    hasDehydratedOnlineOnly = true;
+            isFolderOrMultiple = QFileInfo(fileData.localPath).isDir();
+            auto availability = syncFolder->vfs().availability(fileData.folderRelativePath);
+            if (!availability)
+                availability = VfsItemAvailability::SomeDehydrated; // db error
+            if (!combined) {
+                combined = availability;
+            } else {
+                combined = merge(*combined, *availability);
             }
         }
+        ENFORCE(combined);
+        if (files.size() > 1)
+            isFolderOrMultiple = true;
 
-        auto makePinContextMenu = [listener](QString currentState, QString availableLocally, QString onlineOnly) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:") + currentState);
-            if (!availableLocally.isEmpty())
-                listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY::") + availableLocally);
-            if (!onlineOnly.isEmpty())
-                listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY::") + onlineOnly);
+        // TODO: Should be a submenu, should use icons
+        auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
+                                  + vfsItemAvailabilityToString(*combined, isFolderOrMultiple));
+            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
+                                  + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
+                                  + tr("Make always available locally"));
+            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
+                                  + (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
+                                  + tr("Free up local space"));
         };
 
-        // TODO: Should be a submenu, should use menu item checkmarks where available, should use icons
-        if (hasAlwaysLocal) {
-            if (!hasOnlineOnly) {
-                makePinContextMenu(
-                    tr("Currently available locally"),
-                    QString(),
-                    tr("Make available online only"));
-            } else { // local + online
-                makePinContextMenu(
-                    tr("Current availability is mixed"),
-                    tr("Make all available locally"),
-                    tr("Make all available online only"));
-            }
-        } else if (hasOnlineOnly) {
-            if (hasDehydratedOnlineOnly && !hasHydratedOnlineOnly) {
-                makePinContextMenu(
-                    tr("Currently available online only"),
-                    tr("Make available locally"),
-                    QString());
-            } else if (hasHydratedOnlineOnly && !hasDehydratedOnlineOnly) {
-                makePinContextMenu(
-                    tr("Currently available, but marked online only"),
-                    tr("Make available locally"),
-                    tr("Make available online only"));
-            } else { // hydrated + dehydrated
-                makePinContextMenu(
-                    tr("Some currently available, all marked online only"),
-                    tr("Make available locally"),
-                    tr("Make available online only"));
-            }
+        switch (*combined) {
+        case VfsItemAvailability::AlwaysLocal:
+            makePinContextMenu(false, true);
+            break;
+        case VfsItemAvailability::AllHydrated:
+            makePinContextMenu(true, true);
+            break;
+        case VfsItemAvailability::SomeDehydrated:
+            makePinContextMenu(true, true);
+            break;
+        case VfsItemAvailability::OnlineOnly:
+            makePinContextMenu(true, false);
+            break;
         }
     }
 
diff --git a/src/libsync/vfs/suffix/vfs_suffix.cpp b/src/libsync/vfs/suffix/vfs_suffix.cpp
index 9860c24b2..0480d02b2 100644
--- a/src/libsync/vfs/suffix/vfs_suffix.cpp
+++ b/src/libsync/vfs/suffix/vfs_suffix.cpp
@@ -105,4 +105,13 @@ bool VfsSuffix::statTypeVirtualFile(csync_file_stat_t *stat, void *)
     return false;
 }
 
+Optional<VfsItemAvailability> VfsSuffix::availability(const QString &folderPath)
+{
+    const auto suffix = fileSuffix();
+    QString pinPath = folderPath;
+    if (pinPath.endsWith(suffix))
+        pinPath.chop(suffix.size());
+    return availabilityInDb(folderPath, pinPath);
+}
+
 } // namespace OCC
diff --git a/src/libsync/vfs/suffix/vfs_suffix.h b/src/libsync/vfs/suffix/vfs_suffix.h
index 6c42bcfb4..5aadf5449 100644
--- a/src/libsync/vfs/suffix/vfs_suffix.h
+++ b/src/libsync/vfs/suffix/vfs_suffix.h
@@ -51,6 +51,7 @@ public:
     { return setPinStateInDb(folderPath, state); }
     Optional<PinState> pinState(const QString &folderPath) override
     { return pinStateInDb(folderPath); }
+    Optional<VfsItemAvailability> availability(const QString &folderPath) override;
 
 public slots:
     void fileStatusChanged(const QString &, SyncFileStatus) override {}
diff --git a/test/testsyncjournaldb.cpp b/test/testsyncjournaldb.cpp
index d8d663b9c..fd404559b 100644
--- a/test/testsyncjournaldb.cpp
+++ b/test/testsyncjournaldb.cpp
@@ -336,6 +336,14 @@ private slots:
             }
             return *state;
         };
+        auto getRecursive = [&](const QByteArray &path) -> PinState {
+            auto state = _db.internalPinStates().effectiveForPathRecursive(path);
+            if (!state) {
+                QTest::qFail("couldn't read pin state", __FILE__, __LINE__);
+                return PinState::Inherited;
+            }
+            return *state;
+        };
         auto getRaw = [&](const QByteArray &path) -> PinState {
             auto state = _db.internalPinStates().rawForPath(path);
             if (!state) {
@@ -370,6 +378,7 @@ private slots:
         QCOMPARE(list->size(), 4 + 9 + 27);
 
         // Baseline direct checks (the fallback for unset root pinstate is AlwaysLocal)
+        QCOMPARE(get(""), PinState::AlwaysLocal);
         QCOMPARE(get("local"), PinState::AlwaysLocal);
         QCOMPARE(get("online"), PinState::OnlineOnly);
         QCOMPARE(get("inherit"), PinState::AlwaysLocal);
@@ -399,6 +408,20 @@ private slots:
         QCOMPARE(get("online/online/inherit"), PinState::OnlineOnly);
         QCOMPARE(get("online/online/nonexistant"), PinState::OnlineOnly);
 
+        // Spot check the recursive variant
+        QCOMPARE(getRecursive(""), PinState::Inherited);
+        QCOMPARE(getRecursive("local"), PinState::Inherited);
+        QCOMPARE(getRecursive("online"), PinState::Inherited);
+        QCOMPARE(getRecursive("inherit"), PinState::Inherited);
+        QCOMPARE(getRecursive("online/local"), PinState::Inherited);
+        QCOMPARE(getRecursive("online/local/inherit"), PinState::AlwaysLocal);
+        QCOMPARE(getRecursive("inherit/inherit/inherit"), PinState::AlwaysLocal);
+        QCOMPARE(getRecursive("inherit/online/inherit"), PinState::OnlineOnly);
+        QCOMPARE(getRecursive("inherit/online/local"), PinState::AlwaysLocal);
+        make("local/local/local/local", PinState::AlwaysLocal);
+        QCOMPARE(getRecursive("local/local/local"), PinState::AlwaysLocal);
+        QCOMPARE(getRecursive("local/local/local/local"), PinState::AlwaysLocal);
+
         // Check changing the root pin state
         make("", PinState::OnlineOnly);
         QCOMPARE(get("local"), PinState::AlwaysLocal);
diff --git a/test/testsyncvirtualfiles.cpp b/test/testsyncvirtualfiles.cpp
index afb737c6e..86845bc8e 100644
--- a/test/testsyncvirtualfiles.cpp
+++ b/test/testsyncvirtualfiles.cpp
@@ -1024,6 +1024,66 @@ private slots:
         QVERIFY(!fakeFolder.currentLocalState().find("A/file2.nextcloud.nextcloud"));
         cleanup();
     }
+
+    void testAvailability()
+    {
+        FakeFolder fakeFolder{ FileInfo() };
+        auto vfs = setupVfs(fakeFolder);
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        auto setPin = [&] (const QByteArray &path, PinState state) {
+            fakeFolder.syncJournal().internalPinStates().setForPath(path, state);
+        };
+
+        fakeFolder.remoteModifier().mkdir("local");
+        fakeFolder.remoteModifier().mkdir("local/sub");
+        fakeFolder.remoteModifier().mkdir("online");
+        fakeFolder.remoteModifier().mkdir("online/sub");
+        fakeFolder.remoteModifier().mkdir("unspec");
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        setPin("local", PinState::AlwaysLocal);
+        setPin("online", PinState::OnlineOnly);
+        setPin("unspec", PinState::Unspecified);
+
+        fakeFolder.remoteModifier().insert("file1");
+        fakeFolder.remoteModifier().insert("online/file1");
+        fakeFolder.remoteModifier().insert("online/file2");
+        fakeFolder.remoteModifier().insert("local/file1");
+        fakeFolder.remoteModifier().insert("local/file2");
+        fakeFolder.remoteModifier().insert("unspec/file1");
+        QVERIFY(fakeFolder.syncOnce());
+
+        // root is unspecified
+        QCOMPARE(*vfs->availability("file1"), VfsItemAvailability::AllHydrated);
+        QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AlwaysLocal);
+        QCOMPARE(*vfs->availability("local/file1"), VfsItemAvailability::AlwaysLocal);
+        QCOMPARE(*vfs->availability("online"), VfsItemAvailability::OnlineOnly);
+        QCOMPARE(*vfs->availability("online/file1.nextcloud"), VfsItemAvailability::OnlineOnly);
+        QCOMPARE(*vfs->availability("unspec"), VfsItemAvailability::SomeDehydrated);
+        QCOMPARE(*vfs->availability("unspec/file1.nextcloud"), VfsItemAvailability::SomeDehydrated);
+
+        // Subitem pin states can ruin "pure" availabilities
+        setPin("local/sub", PinState::OnlineOnly);
+        QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AllHydrated);
+        setPin("online/sub", PinState::Unspecified);
+        QCOMPARE(*vfs->availability("online"), VfsItemAvailability::SomeDehydrated);
+
+        triggerDownload(fakeFolder, "unspec/file1");
+        setPin("local/file2", PinState::OnlineOnly);
+        QVERIFY(fakeFolder.syncOnce());
+
+        QCOMPARE(*vfs->availability("unspec"), VfsItemAvailability::AllHydrated);
+        QCOMPARE(*vfs->availability("local"), VfsItemAvailability::SomeDehydrated);
+
+        vfs->setPinState("local", PinState::AlwaysLocal);
+        vfs->setPinState("online", PinState::OnlineOnly);
+        QVERIFY(fakeFolder.syncOnce());
+
+        QCOMPARE(*vfs->availability("online"), VfsItemAvailability::OnlineOnly);
+        QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AlwaysLocal);
+    }
 };
 
 QTEST_GUILESS_MAIN(TestSyncVirtualFiles)