Merge branch 'claude/vault-creation-sharing-62c0b6'
ci / build and test (push) Successful in 1m33s
ci / android head (push) Failing after 5s
ci / api image (push) Successful in 23s

This commit is contained in:
2026-08-03 21:53:09 +02:00
21 changed files with 2340 additions and 80 deletions
@@ -63,6 +63,24 @@ internal sealed partial class FakeVaultServer : ITeamApi, IDirectoryApi, IVaultG
/// </remarks>
internal bool CorruptKeyLog { get; set; }
/// <summary>Slugs this fake refuses, as the real server refuses one already in use.</summary>
/// <remarks>
/// A vault's slug is derived from its name rather than typed, so a collision is something the client
/// has to get out of on its own — and a fake that accepted every slug could not tell whether it does.
/// </remarks>
internal HashSet<string> TakenSlugs { get; } = new(StringComparer.Ordinal);
/// <summary>How many vault creates to refuse before answering normally.</summary>
/// <remarks>
/// Creating a vault of its own is two calls, and the failure worth testing is the one between them:
/// the team is made and the vault is not. One refusal is enough to leave the client in that state and
/// let the test press CREATE again.
/// </remarks>
internal int VaultCreateFailures { get; set; }
/// <summary>How many team creates have been asked for, for a test to assert on.</summary>
internal int TeamCreates { get; private set; }
/// <summary>Registers another account, as though they had signed in and enrolled here.</summary>
/// <returns>Their user id.</returns>
internal Guid AddAccount(string email, string displayName)
@@ -119,6 +137,24 @@ internal sealed partial class FakeVaultServer : ITeamApi, IDirectoryApi, IVaultG
CreateTeamRequest request,
CancellationToken cancellationToken)
{
TeamCreates++;
// Idempotent on the client-chosen id, as the real one is. That is the whole of how a create whose
// second half failed is retried without leaving a second team behind, so a fake that made one
// anyway would let the bug through.
if (teams.Find(row => row.TeamId == request.TeamId) is { } existing)
{
return Task.FromResult(existing);
}
if (TakenSlugs.Contains(request.Slug))
{
throw new DodoSshApiException(
System.Net.HttpStatusCode.Conflict,
ProblemCodes.TeamSlugTaken,
$"The slug '{request.Slug}' is already in use.");
}
var team = new TeamSummary(
request.TeamId,
request.Name,
@@ -433,6 +469,16 @@ internal sealed partial class FakeVaultServer : ITeamApi, IDirectoryApi, IVaultG
CreateTeamVaultRequest request,
CancellationToken cancellationToken)
{
if (VaultCreateFailures > 0)
{
VaultCreateFailures--;
throw new DodoSshApiException(
System.Net.HttpStatusCode.ServiceUnavailable,
code: null,
"The server is not answering.");
}
var vault = new VaultSummary(
request.VaultId,
request.Name,