diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 4619e8f..afdf90f 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -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. diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index dbc969d..872c86c 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -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 diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs index c015b9e..f5ce556 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs @@ -35,6 +35,7 @@ public interface ICloudStateStore IDictionary? meta); IReadOnlyList 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 GetBinaryRequests(); IReadOnlyList GetHolidays(); void UpdateRobot(DeviceRegistration registration); -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs index b70182b..ec771fd 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs @@ -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()); + 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()); } private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope) @@ -631,4 +639,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore) bool.TryParse(value, out var parsed) && parsed; } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs index 683d411..d4a374f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs @@ -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 }; } } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs index 9bf261b..f954297 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs @@ -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)); } -} \ No newline at end of file +}