Implement update backup and restore proof

This commit is contained in:
Jacob Dubin
2026-05-17 10:11:36 -05:00
parent dfcf521a5a
commit 3b279fdd6f
6 changed files with 95 additions and 11 deletions

View File

@@ -891,7 +891,7 @@ For `1.0.19`:
- store contracts are now tightened around account/loop/device/person scoping, revision tracking, and explicit load/save boundaries
- the backend seam is now selectable, with file-backed local persistence as default and an Azure Blob Storage slot wired for future deployment when a storage account connection string is available
- next implementation pass should supply the real Azure Storage connection string / deployment wiring and validate the live round-trip in the storage account smoke test
10. Update, backup, and restore proof
10. Update, backup, and restore proof - implemented (update creation and backup creation now survive persisted reloads; restore proof uses the same persisted state rehydration path)
11. STT upgrade and noise screening
12. Hosted capture/storage plan / indexing for group testing
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.

View File

@@ -338,7 +338,7 @@ First completed slice in this personal-report parity track:
4. Personal report parity slices
5. Holidays and seasonal personality slice beyond pizza day
6. Durable memory persistence path
7. Update/backup/restore end-to-end proof
7. Update/backup/restore end-to-end proof - implemented
8. STT noise-screening and short-utterance reliability pass
9. Provider-backed news expansion and deeper weather parity
10. Capture indexing and retention boundary for group testing

View File

@@ -35,6 +35,7 @@ public interface ICloudStateStore
IDictionary<string, object?>? meta);
IReadOnlyList<BackupRecord> GetBackups();
BackupRecord CreateBackup(string name);
bool ShouldCreateSymmetricKey(string loopId);
string GetOrCreateSymmetricKey(string loopId);
KeyRequestRecord CreateKeyRequest(string loopId, string publicKey);
@@ -43,4 +44,4 @@ public interface ICloudStateStore
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
IReadOnlyList<object> GetHolidays();
void UpdateRobot(DeviceRegistration registration);
}
}

View File

@@ -51,7 +51,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
return Task.FromResult(HandleLog(operation, envelope));
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
return Task.FromResult(HandleBackup(operation));
return Task.FromResult(HandleBackup(operation, envelope));
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
return Task.FromResult(HandleAccount(operation, envelope));
@@ -353,11 +353,19 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
: []);
}
private ProtocolDispatchResult HandleBackup(string operation)
private ProtocolDispatchResult HandleBackup(string operation, ProtocolEnvelope envelope)
{
return operation.Equals("List", StringComparison.OrdinalIgnoreCase)
? ProtocolDispatchResult.Ok(stateStore.GetBackups())
: ProtocolDispatchResult.Ok(Array.Empty<object>());
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
return ProtocolDispatchResult.Ok(stateStore.GetBackups());
if (operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
{
var body = envelope.TryParseBody();
var requestedName = ReadString(body, "name") ?? ReadString(body, "backupName");
return ProtocolDispatchResult.Ok(stateStore.CreateBackup(requestedName ?? $"backup-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"));
}
return ProtocolDispatchResult.Ok(Array.Empty<object>());
}
private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)
@@ -631,4 +639,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
bool.TryParse(value, out var parsed) &&
parsed;
}
}
}

View File

@@ -452,6 +452,18 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
return _backups.ToArray();
}
public BackupRecord CreateBackup(string name)
{
var backup = new BackupRecord
{
Name = string.IsNullOrWhiteSpace(name) ? "backup" : name.Trim()
};
_backups.Add(backup);
TouchState();
return backup;
}
public bool ShouldCreateSymmetricKey(string loopId)
{
return !_symmetricKeys.ContainsKey(loopId);
@@ -661,4 +673,4 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
};
}
}
}
}

View File

@@ -148,6 +148,69 @@ public sealed class JiboCloudProtocolServiceTests
}
}
[Fact]
public async Task UpdateAndBackupPersistAcrossStoreRecreation_WhenPersistencePathIsConfigured()
{
var persistencePath = Path.Combine(Path.GetTempPath(), $"openjibo-update-backup-{Guid.NewGuid():N}.json");
try
{
var firstService = new JiboCloudProtocolService(new InMemoryCloudStateStore(persistencePath));
await firstService.DispatchAsync(new ProtocolEnvelope
{
HostName = "api.jibo.com",
Method = "POST",
ServicePrefix = "Update_20160715",
Operation = "CreateUpdate",
BodyText = """{"fromVersion":"1.0.0","toVersion":"1.0.1","changes":"Restore proof","subsystem":"robot"}"""
});
await firstService.DispatchAsync(new ProtocolEnvelope
{
HostName = "api.jibo.com",
Method = "POST",
ServicePrefix = "Backup_20160715",
Operation = "Create",
BodyText = """{"name":"manual-backup"}"""
});
var secondService = new JiboCloudProtocolService(new InMemoryCloudStateStore(persistencePath));
var updates = await secondService.DispatchAsync(new ProtocolEnvelope
{
HostName = "api.jibo.com",
Method = "POST",
ServicePrefix = "Update_20160715",
Operation = "ListUpdates",
BodyText = """{"subsystem":"robot"}"""
});
var backups = await secondService.DispatchAsync(new ProtocolEnvelope
{
HostName = "api.jibo.com",
Method = "POST",
ServicePrefix = "Backup_20160715",
Operation = "List",
BodyText = "{}"
});
using var updatesPayload = JsonDocument.Parse(updates.BodyText);
using var backupsPayload = JsonDocument.Parse(backups.BodyText);
Assert.NotEmpty(updatesPayload.RootElement.EnumerateArray());
Assert.Contains(updatesPayload.RootElement.EnumerateArray(), item => item.GetProperty("changes").GetString() == "Restore proof");
Assert.NotEmpty(backupsPayload.RootElement.EnumerateArray());
Assert.Contains(backupsPayload.RootElement.EnumerateArray(), item => item.TryGetProperty("Name", out var name) && name.GetString() == "manual-backup");
}
finally
{
if (File.Exists(persistencePath))
{
File.Delete(persistencePath);
}
}
}
[Fact]
public async Task MediaCreate_StoresBodyAndServesMediaUrl()
{
@@ -230,4 +293,4 @@ public sealed class JiboCloudProtocolServiceTests
Assert.Contains(people,
person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase));
}
}
}