mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(mobile): persist Solo retries and show their saved recipient
Rewrite the recovery journal after failed cached preference writes. Render the saved agent identity in locked native retries even when discovery has not returned it. Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ struct MeAgentAccessibilityTests {
|
||||
@MainActor static func main() {
|
||||
for locked in [false, true] {
|
||||
for empty in [false, true] {
|
||||
let savedName = "Saved agent"
|
||||
var submitted: [String: String]?
|
||||
let controller = NativeProfileTextEditorViewController(
|
||||
title: "New chat", initialValue: "Chat", placeholder: "Chat name",
|
||||
@@ -15,7 +16,7 @@ struct MeAgentAccessibilityTests {
|
||||
channelDescription: nil, originalName: nil, originalDescription: nil,
|
||||
canEditDetails: true, canEditCanvas: false, canvasContent: "",
|
||||
canvasLoaded: false,
|
||||
chatOptions: ["selectedAssistant": "agent", "presentationId": "test", "assistantsLoading": empty, "fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
|
||||
chatOptions: ["selectedAssistant": "agent", "savedAssistant": ["id": "agent", "name": savedName], "presentationId": "test", "assistantsLoading": empty, "fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
|
||||
onCancel: {}, onSet: { submitted = $0 as? [String: String] }
|
||||
)
|
||||
controller.loadViewIfNeeded()
|
||||
@@ -28,6 +29,9 @@ struct MeAgentAccessibilityTests {
|
||||
precondition(project.isEnabled == !locked)
|
||||
precondition(project.showsMenuAsPrimaryAction && project.menu != nil)
|
||||
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||
let content = cell.contentConfiguration as! UIListContentConfiguration
|
||||
precondition(content.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
|
||||
precondition((content.image != nil) == (locked || !empty))
|
||||
let disabled = locked || empty
|
||||
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
|
||||
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
|
||||
@@ -58,6 +62,9 @@ struct MeAgentAccessibilityTests {
|
||||
"presentationId": "test",
|
||||
"assistants": empty ? [] : [["id": "agent", "name": "Agent"]],
|
||||
])
|
||||
let refreshedCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||
let refreshed = refreshedCell.contentConfiguration as! UIListContentConfiguration
|
||||
precondition(refreshed.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
|
||||
precondition(controller.navigationItem.rightBarButtonItem?.isEnabled == canSubmit)
|
||||
_ = controller.perform(NSSelectorFromString("setTapped"))
|
||||
precondition(submitted?["assistant"] == (canSubmit ? "agent" : nil))
|
||||
|
||||
@@ -632,8 +632,13 @@ private final class NativeProfileTextEditorViewController:
|
||||
cell.accessoryView = button
|
||||
} else if isChatEditor && indexPath.section == 2 {
|
||||
var content = cell.defaultContentConfiguration()
|
||||
let selected = assistants.first { $0["id"] == selectedAssistant }
|
||||
let saved = chatOptions?["savedAssistant"] as? [String: String]
|
||||
let fallback = fieldsLocked ? selectedAssistant.map {
|
||||
saved.flatMap { $0["id"] == selectedAssistant ? $0 : nil } ?? ["id": $0, "name": $0]
|
||||
} : nil
|
||||
let selected = assistants.first { $0["id"] == selectedAssistant } ?? fallback
|
||||
content.text = selected?["name"] ?? "Choose an agent"
|
||||
content.textProperties.numberOfLines = 0
|
||||
if let selected {
|
||||
content.image = selectedAssistantAvatar ?? MeAssistantListController.avatarImage(selected, photo: nil)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,19 @@ Future<(Channel, String)?> _showNewMeChat(
|
||||
.toARGB32(),
|
||||
'containerCornerRadius': Radii.container,
|
||||
'selectedAssistant': assistant,
|
||||
if (retry && assistant != null)
|
||||
'savedAssistant': {
|
||||
'id': assistant,
|
||||
'name':
|
||||
ref.read(userCacheProvider)[assistant]?.label ??
|
||||
directory
|
||||
?.where((a) => a.pubkey == assistant)
|
||||
.firstOrNull
|
||||
?.displayName ??
|
||||
assistant,
|
||||
'avatarUrl':
|
||||
ref.read(userCacheProvider)[assistant]?.avatarUrl ?? '',
|
||||
},
|
||||
'selectedProject': project,
|
||||
'projects': [
|
||||
for (final group
|
||||
|
||||
@@ -50,7 +50,7 @@ class MeChatCreation {
|
||||
previous.project != project)) {
|
||||
throw StateError('Resume the pending chat before starting another.');
|
||||
}
|
||||
if (previous != null) return;
|
||||
// A matching cached value does not prove a previous disk write succeeded.
|
||||
if (sync.prefs == null ||
|
||||
!await sync.prefs!.setString(
|
||||
storageKey(sync),
|
||||
|
||||
@@ -157,6 +157,7 @@ void main() {
|
||||
final draft = Completer<Map<String, String>>();
|
||||
String? presentationId;
|
||||
Map<Object?, Object?>? agentUpdate;
|
||||
Object? savedAssistant;
|
||||
var forms = 0;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(bridge, (
|
||||
call,
|
||||
@@ -166,7 +167,9 @@ void main() {
|
||||
return null;
|
||||
}
|
||||
if (++forms > 1) return null;
|
||||
presentationId = (call.arguments as Map)['presentationId'] as String;
|
||||
final arguments = call.arguments as Map;
|
||||
presentationId = arguments['presentationId'] as String;
|
||||
savedAssistant = arguments['savedAssistant'];
|
||||
return draft.future;
|
||||
});
|
||||
addTearDown(
|
||||
@@ -216,6 +219,13 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
expect(forms, 1, reason: 'Compose opens even while agents are loading');
|
||||
if (recoverExisting) {
|
||||
expect(savedAssistant, {
|
||||
'id': agent.public,
|
||||
'name': agent.public,
|
||||
'avatarUrl': '',
|
||||
});
|
||||
}
|
||||
directory.complete([
|
||||
AgentDirectoryEntry(pubkey: agent.public, displayName: 'Owned agent'),
|
||||
]);
|
||||
|
||||
@@ -1,11 +1,59 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
|
||||
import 'package:buzz/shared/me/me_chat_creation.dart';
|
||||
import 'package:buzz/shared/me/me_sync.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'retry confirms persistence after a failed cached journal write',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final store = _FailingStore();
|
||||
final previousStore = SharedPreferencesStorePlatform.instance;
|
||||
SharedPreferencesStorePlatform.instance = store;
|
||||
addTearDown(
|
||||
() => SharedPreferencesStorePlatform.instance = previousStore,
|
||||
);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sync = MeSync(
|
||||
RelaySessionNotifier(),
|
||||
RelayConfig(
|
||||
baseUrl: 'https://relay.test',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
),
|
||||
prefs: prefs,
|
||||
);
|
||||
const intent = MeChatCreation('id', 'Name', 'agent');
|
||||
var reachedRelay = false;
|
||||
Future<String> run() => intent.complete<String>(
|
||||
sync,
|
||||
findChannel: () async {
|
||||
reachedRelay = true;
|
||||
await prefs.reload();
|
||||
expect(MeChatCreation.pending(sync)?.id, intent.id);
|
||||
return null;
|
||||
},
|
||||
createChannel: () async => throw StateError('Simulated process stop'),
|
||||
hasAgent: () async => false,
|
||||
addAgent: () async {},
|
||||
isPlaced: () async => false,
|
||||
place: () async {},
|
||||
);
|
||||
await expectLater(run(), throwsStateError);
|
||||
expect(reachedRelay, isFalse);
|
||||
expect(
|
||||
MeChatCreation.pending(sync)?.id,
|
||||
intent.id,
|
||||
); // Failed write remains cached.
|
||||
await expectLater(run(), throwsStateError);
|
||||
expect(reachedRelay, isTrue);
|
||||
await prefs.reload();
|
||||
expect(MeChatCreation.pending(sync)?.id, intent.id);
|
||||
},
|
||||
);
|
||||
test('missing durable storage blocks every relay side effect', () async {
|
||||
final sync = MeSync(
|
||||
RelaySessionNotifier(),
|
||||
@@ -150,3 +198,16 @@ void main() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FailingStore extends InMemorySharedPreferencesStore {
|
||||
_FailingStore() : super.empty();
|
||||
bool failNext = true;
|
||||
@override
|
||||
Future<bool> setValue(String valueType, String key, Object value) async {
|
||||
if (failNext && key.startsWith('flutter.me-chat-creation:')) {
|
||||
failNext = false;
|
||||
return false;
|
||||
}
|
||||
return super.setValue(valueType, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user