Merge branch 'claude/main-page-group-hierarchy-3a3210'
ci / build and test (push) Successful in 1m27s
ci / android head (push) Failing after 5s
ci / api image (push) Successful in 21s

This commit is contained in:
2026-08-04 10:26:20 +02:00
8 changed files with 214 additions and 33 deletions
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using DodoSSH.Client.Domain;
using DodoSSH.Client.Storage;
using DodoSSH.Client.Sync;
using DodoSSH.Contracts;
using DodoSSH.Crypto;
@@ -151,6 +152,63 @@ public sealed class AadResourceTypeTests
.ShouldBe(ItemKinds.SyncedTypes, ignoreOrder: true);
}
/// <summary>
/// The same pairing, made a second time in the storage layer, and every type must be in it.
/// </summary>
/// <remarks>
/// <para>
/// <c>AadResourceTypes.For</c> is the cache's copy of the table above: the ciphers seal an item for the
/// <em>server</em>, and this seals the two things the local cache holds in the clear — a relay host's
/// address, and the values a merge overrode. A type missing from it throws rather than mis-seals, which
/// sounds like the safe failure and is not: <c>ConflictStore.RecordAsync</c> calls it unconditionally, so
/// the exception lands on the path that records what a merge discarded.
/// </para>
/// <para>
/// This is written after finding three types missing from it — <c>ConnectionLogEntry</c>,
/// <c>ActivityLogEntry</c> and <c>ObjectStore</c> went two shipping phases without an arm, because the
/// only unconditional caller is one a test suite reaches solely by causing a real merge conflict. Asserted
/// over the whole wire enum rather than over <c>ItemKinds.SyncedTypes</c>, which is the stronger claim and
/// the one the switch actually makes: the two reserved association types have arms too.
/// </para>
/// </remarks>
[Fact]
public void EverySyncableType_HasAnArmInTheStorageMapping()
{
var missing = new List<SyncEntityType>();
foreach (var wire in Enum.GetValues<SyncEntityType>())
{
if (wire == SyncEntityType.Unspecified)
{
continue;
}
CryptoSpec.AadResourceType resource;
try
{
resource = AadResourceTypes.For(wire);
}
catch (ArgumentOutOfRangeException)
{
missing.Add(wire);
continue;
}
// Same name, as the cipher table demands — a wrong-but-present arm is the failure this half
// would otherwise wave through.
Enum.GetName(resource).ShouldBe(
Enum.GetName(wire),
$"AadResourceTypes.For({wire}) returns {resource}, which binds this type's cache records "
+ "to another type's resource.");
}
missing.ShouldBeEmpty(
"every syncable type needs an arm in AadResourceTypes.For, or a conflict recorded against one "
+ "of these throws instead of being written — and the conflict log is what justifies the merge "
+ "picking a winner.");
}
private static EncryptedPayload SealSample(
SyncEntityType wire,
byte[] vaultKey,