Implement update backup and restore proof
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user