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 - 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 - 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 - 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 11. STT upgrade and noise screening
12. Hosted capture/storage plan / indexing for group testing 12. Hosted capture/storage plan / indexing for group testing
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc. 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 4. Personal report parity slices
5. Holidays and seasonal personality slice beyond pizza day 5. Holidays and seasonal personality slice beyond pizza day
6. Durable memory persistence path 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 8. STT noise-screening and short-utterance reliability pass
9. Provider-backed news expansion and deeper weather parity 9. Provider-backed news expansion and deeper weather parity
10. Capture indexing and retention boundary for group testing 10. Capture indexing and retention boundary for group testing

View File

@@ -35,6 +35,7 @@ public interface ICloudStateStore
IDictionary<string, object?>? meta); IDictionary<string, object?>? meta);
IReadOnlyList<BackupRecord> GetBackups(); IReadOnlyList<BackupRecord> GetBackups();
BackupRecord CreateBackup(string name);
bool ShouldCreateSymmetricKey(string loopId); bool ShouldCreateSymmetricKey(string loopId);
string GetOrCreateSymmetricKey(string loopId); string GetOrCreateSymmetricKey(string loopId);
KeyRequestRecord CreateKeyRequest(string loopId, string publicKey); KeyRequestRecord CreateKeyRequest(string loopId, string publicKey);

View File

@@ -51,7 +51,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
return Task.FromResult(HandleLog(operation, envelope)); return Task.FromResult(HandleLog(operation, envelope));
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase)) if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
return Task.FromResult(HandleBackup(operation)); return Task.FromResult(HandleBackup(operation, envelope));
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase)) if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
return Task.FromResult(HandleAccount(operation, envelope)); 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) if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
? ProtocolDispatchResult.Ok(stateStore.GetBackups()) return ProtocolDispatchResult.Ok(stateStore.GetBackups());
: ProtocolDispatchResult.Ok(Array.Empty<object>());
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) private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)

View File

@@ -452,6 +452,18 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
return _backups.ToArray(); 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) public bool ShouldCreateSymmetricKey(string loopId)
{ {
return !_symmetricKeys.ContainsKey(loopId); return !_symmetricKeys.ContainsKey(loopId);

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] [Fact]
public async Task MediaCreate_StoresBodyAndServesMediaUrl() public async Task MediaCreate_StoresBodyAndServesMediaUrl()
{ {