mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
Merge main and preserve asynchronous Solo agent loading
Keep project selection from main and refresh the agent row at its new section. Cover the combined native loading flow in the existing simulator regression. Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -14,15 +14,39 @@ struct MeAgentAccessibilityTests {
|
||||
channelDescription: nil, originalName: nil, originalDescription: nil,
|
||||
canEditDetails: true, canEditCanvas: false, canvasContent: "",
|
||||
canvasLoaded: false,
|
||||
chatOptions: ["fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
|
||||
chatOptions: ["presentationId": "test", "assistantsLoading": empty, "fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
|
||||
onCancel: {}, onSet: { _ in }
|
||||
)
|
||||
controller.loadViewIfNeeded()
|
||||
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
|
||||
precondition(controller.numberOfSections(in: controller.tableView) == 3)
|
||||
let projectCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
|
||||
let project = projectCell.accessoryView as! UIButton
|
||||
precondition(project.accessibilityLabel == "Project")
|
||||
precondition(project.accessibilityValue == "No project")
|
||||
precondition(project.titleColor(for: .normal) == .label)
|
||||
precondition(project.isEnabled == !locked)
|
||||
precondition(project.showsMenuAsPrimaryAction && project.menu != nil)
|
||||
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||
let disabled = locked || empty
|
||||
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
|
||||
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
|
||||
precondition((cell.selectionStyle == .none) == disabled)
|
||||
if empty && !locked {
|
||||
controller.tableView.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||
controller.tableView.reloadData()
|
||||
controller.tableView.layoutIfNeeded()
|
||||
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 1) == nil)
|
||||
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == "Loading agents…")
|
||||
controller.updateChatAssistants([
|
||||
"presentationId": "test",
|
||||
"assistants": [["id": "agent", "name": "Agent"]],
|
||||
])
|
||||
controller.tableView.layoutIfNeeded()
|
||||
let updated = controller.tableView.cellForRow(at: IndexPath(row: 0, section: 2))!
|
||||
precondition(updated.accessibilityTraits.contains(.button))
|
||||
precondition(!updated.accessibilityTraits.contains(.notEnabled))
|
||||
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
avatarTests()
|
||||
|
||||
@@ -256,6 +256,10 @@ private final class NativeProfileTextEditorViewController:
|
||||
private var hasFocusedEditor = false
|
||||
private var selectedAssistantAvatar: UIImage?
|
||||
private var selectedAssistant: String?
|
||||
private var selectedProject: String?
|
||||
private var projects: [[String: String]] {
|
||||
chatOptions?["projects"] as? [[String: String]] ?? []
|
||||
}
|
||||
private var isChatEditor: Bool { chatOptions != nil }
|
||||
private var isProjectEditor: Bool { chatOptions?["projectOnly"] as? Bool ?? false }
|
||||
private var assistants: [[String: String]] {
|
||||
@@ -346,6 +350,7 @@ private final class NativeProfileTextEditorViewController:
|
||||
self.canvasLoaded = canvasLoaded
|
||||
self.chatOptions = chatOptions
|
||||
self.selectedAssistant = chatOptions?["selectedAssistant"] as? String
|
||||
self.selectedProject = chatOptions?["selectedProject"] as? String
|
||||
self.onCancel = onCancel
|
||||
self.onSet = onSet
|
||||
super.init(style: .insetGrouped)
|
||||
@@ -440,7 +445,7 @@ private final class NativeProfileTextEditorViewController:
|
||||
chatOptions?["assistants"] = arguments["assistants"]
|
||||
chatOptions?["assistantsLoading"] = false
|
||||
chatOptions?["assistantError"] = arguments["assistantError"]
|
||||
tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||
tableView.reloadSections(IndexSet(integer: 2), with: .none)
|
||||
updateNavigation()
|
||||
}
|
||||
|
||||
@@ -495,7 +500,7 @@ private final class NativeProfileTextEditorViewController:
|
||||
assistants.contains(where: { $0["id"] == selectedAssistant }),
|
||||
!currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
currentValue.count <= 120 else { return }
|
||||
onSet(["name": currentValue, "assistant": selectedAssistant])
|
||||
onSet(["name": currentValue, "assistant": selectedAssistant, "project": selectedProject ?? ""])
|
||||
} else if isChannelEditor {
|
||||
guard !canonicalChannelName.isEmpty else { return }
|
||||
onSet(["action": "save", "name": canonicalChannelName, "description": textView.text ?? ""])
|
||||
@@ -526,13 +531,13 @@ private final class NativeProfileTextEditorViewController:
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if isChatEditor && indexPath.section == 1 {
|
||||
if isChatEditor && indexPath.section == 2 {
|
||||
guard !fieldsLocked && !assistants.isEmpty else { return }
|
||||
view.endEditing(true)
|
||||
let picker = MeAssistantListController(assistants: assistants, selected: selectedAssistant, pageColor: pageBackgroundColor, rowColor: containerBackgroundColor, avatars: assistantAvatars) { [weak self] id, avatar in
|
||||
self?.selectedAssistantAvatar = avatar
|
||||
self?.selectedAssistant = id
|
||||
self?.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||
self?.tableView.reloadSections(IndexSet(integer: 2), with: .none)
|
||||
self?.updateNavigation()
|
||||
}
|
||||
navigationController?.pushViewController(picker, animated: true)
|
||||
@@ -552,10 +557,10 @@ private final class NativeProfileTextEditorViewController:
|
||||
return false
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { isProjectEditor ? 1 : isChatEditor ? 2 : isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 }
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { isProjectEditor ? 1 : isChatEditor ? 3 : isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
guard isChatEditor && section == 1 else { return nil }
|
||||
guard isChatEditor && section == 2 else { return nil }
|
||||
if chatOptions?["assistantsLoading"] as? Bool == true { return "Loading agents…" }
|
||||
if let error = chatOptions?["assistantError"] as? String { return error }
|
||||
if let error = chatOptions?["error"] as? String { return error }
|
||||
@@ -573,7 +578,7 @@ private final class NativeProfileTextEditorViewController:
|
||||
_ tableView: UITableView,
|
||||
heightForRowAt indexPath: IndexPath
|
||||
) -> CGFloat {
|
||||
if isChatEditor && indexPath.section == 1 && selectedAssistant != nil { return 64 }
|
||||
if isChatEditor && indexPath.section == 2 && selectedAssistant != nil { return 64 }
|
||||
if isChannelEditor && indexPath.section >= 2 { return UITableView.automaticDimension }
|
||||
return (isChannelEditor ? indexPath.section == 1 : multiline) ? 132 : 52
|
||||
}
|
||||
@@ -589,6 +594,36 @@ private final class NativeProfileTextEditorViewController:
|
||||
cell.layer.cornerCurve = .continuous
|
||||
cell.clipsToBounds = true
|
||||
if isChatEditor && indexPath.section == 1 {
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = "Project"
|
||||
content.textProperties.font = .preferredFont(forTextStyle: .body)
|
||||
cell.contentConfiguration = content
|
||||
let selected = projects.first { $0["id"] == selectedProject }
|
||||
let name = selected?["name"] ?? (selectedProject == nil ? "No project" : "Unavailable project")
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(name, for: .normal)
|
||||
button.setTitleColor(.label, for: .normal)
|
||||
button.setTitleColor(.secondaryLabel, for: .disabled)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .body)
|
||||
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
button.accessibilityLabel = "Project"
|
||||
button.accessibilityValue = name
|
||||
button.isEnabled = !fieldsLocked
|
||||
button.showsMenuAsPrimaryAction = true
|
||||
let choices = [["id": "", "name": "No project"]] + projects
|
||||
button.menu = UIMenu(children: choices.map { project in
|
||||
let id = project["id"] ?? ""
|
||||
return UIAction(title: project["name"] ?? "Project", state: id == (selectedProject ?? "") ? .on : .off) { [weak self] _ in
|
||||
self?.view.endEditing(true)
|
||||
self?.selectedProject = id.isEmpty ? nil : id
|
||||
self?.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||
}
|
||||
})
|
||||
button.titleLabel?.lineBreakMode = .byTruncatingTail
|
||||
button.sizeToFit()
|
||||
button.frame.size.width = min(button.frame.width, tableView.bounds.width * 0.55)
|
||||
cell.accessoryView = button
|
||||
} else if isChatEditor && indexPath.section == 2 {
|
||||
var content = cell.defaultContentConfiguration()
|
||||
let selected = assistants.first { $0["id"] == selectedAssistant }
|
||||
content.text = selected?["name"] ?? "Choose an agent"
|
||||
|
||||
@@ -43,6 +43,7 @@ Future<(Channel, String)?> _showNewMeChat(
|
||||
final id = pending?.id ?? const Uuid().v4();
|
||||
String name = pending?.name ?? '';
|
||||
String? assistant = pending?.agent;
|
||||
String? project = pending?.project;
|
||||
String? error = pending == null ? null : 'Resume your unfinished chat.';
|
||||
var retry = pending != null;
|
||||
while (context.mounted) {
|
||||
@@ -75,6 +76,12 @@ Future<(Channel, String)?> _showNewMeChat(
|
||||
.toARGB32(),
|
||||
'containerCornerRadius': Radii.container,
|
||||
'selectedAssistant': assistant,
|
||||
'selectedProject': project,
|
||||
'projects': [
|
||||
for (final group
|
||||
in ref.read(meGroupsProvider).requireValue.groups)
|
||||
{'id': group['id'], 'name': group['name']},
|
||||
],
|
||||
'fieldsLocked': retry,
|
||||
'error': error,
|
||||
'assistants': _nativeMeAssistants(directory ?? []),
|
||||
@@ -120,10 +127,19 @@ Future<(Channel, String)?> _showNewMeChat(
|
||||
if (draft == null || !context.mounted) return null;
|
||||
name = (draft['name'] as String? ?? '').trim();
|
||||
assistant = draft['assistant'] as String?;
|
||||
project = draft['project'] as String?;
|
||||
if (project == '') project = null;
|
||||
if (name.isEmpty || assistant == null) return null;
|
||||
retry = true;
|
||||
try {
|
||||
final created = await _createMeChat(ref, sync, id, name, assistant);
|
||||
final created = await _createMeChat(
|
||||
ref,
|
||||
sync,
|
||||
id,
|
||||
name,
|
||||
assistant,
|
||||
project,
|
||||
);
|
||||
return (created, assistant);
|
||||
} catch (failure) {
|
||||
if (failure is MeRecoveryConflict && context.mounted) {
|
||||
@@ -150,9 +166,10 @@ Future<Channel> _createMeChat(
|
||||
String channelId,
|
||||
String name,
|
||||
String assistant,
|
||||
String? project,
|
||||
) async {
|
||||
sync = _currentMeSync(ref, sync);
|
||||
final intent = MeChatCreation(channelId, name, assistant);
|
||||
final intent = MeChatCreation(channelId, name, assistant, project: project);
|
||||
await intent.save(sync);
|
||||
final actions = ref.read(channelActionsProvider);
|
||||
final preflight = await Future.wait<Object?>([
|
||||
@@ -167,7 +184,7 @@ Future<Channel> _createMeChat(
|
||||
}
|
||||
final head = preflight[1] as NostrEvent?;
|
||||
(head == null ? const MeGroups() : sync.decode(head))
|
||||
.place(channelId)
|
||||
.place(channelId, group: project)
|
||||
.encode(sync.community);
|
||||
return intent.complete<Channel>(
|
||||
sync,
|
||||
@@ -189,9 +206,13 @@ Future<Channel> _createMeChat(
|
||||
actions.addMembers(channelId: channelId, pubkeys: [assistant]),
|
||||
isPlaced: () async {
|
||||
final latest = await sync.head();
|
||||
return latest != null && sync.decode(latest).channels.contains(channelId);
|
||||
if (latest == null) return false;
|
||||
final groups = sync.decode(latest);
|
||||
return groups.channels.contains(channelId) &&
|
||||
(project == null || groups.assignments[channelId] == project);
|
||||
},
|
||||
place: () => sync.update((current) => current.place(channelId)),
|
||||
place: () =>
|
||||
sync.update((current) => current.place(channelId, group: project)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ class _NewMeChat extends HookConsumerWidget {
|
||||
final pending = useMemoized(() => MeChatCreation.pending(sync));
|
||||
final name = useTextEditingController(text: pending?.name ?? '');
|
||||
final selected = useState<String?>(pending?.agent);
|
||||
final project = useState<String?>(pending?.project);
|
||||
final groups = ref.watch(meGroupsProvider);
|
||||
final channelId = useMemoized(() => pending?.id ?? const Uuid().v4());
|
||||
final frozenName = useState<String?>(pending?.name);
|
||||
final frozenAgent = useState<String?>(pending?.agent);
|
||||
@@ -52,6 +54,34 @@ class _NewMeChat extends HookConsumerWidget {
|
||||
onChanged: (_) => error.value = null,
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
DropdownButtonFormField<String>(
|
||||
key: const ValueKey('new-session-project'),
|
||||
initialValue: project.value ?? '',
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(labelText: 'Project'),
|
||||
items: [
|
||||
const DropdownMenuItem(value: '', child: Text('No project')),
|
||||
for (final group
|
||||
in groups.asData?.value.groups ?? <Map<String, dynamic>>[])
|
||||
DropdownMenuItem(
|
||||
value: group['id'] as String,
|
||||
child: Text(group['name'] as String),
|
||||
),
|
||||
if (project.value != null &&
|
||||
!(groups.asData?.value.groups ?? []).any(
|
||||
(group) => group['id'] == project.value,
|
||||
))
|
||||
DropdownMenuItem(
|
||||
value: project.value,
|
||||
child: const Text('Unavailable project'),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
busy.value || frozenName.value != null || !groups.hasValue
|
||||
? null
|
||||
: (value) => project.value = value == '' ? null : value,
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
AppListRowRaw(
|
||||
key: const ValueKey('new-session-assistant'),
|
||||
leading: chosen == null
|
||||
@@ -130,6 +160,7 @@ class _NewMeChat extends HookConsumerWidget {
|
||||
channelId,
|
||||
frozenName.value!,
|
||||
frozenAgent.value!,
|
||||
project.value,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../crypto/nip44.dart';
|
||||
import 'me_sync.dart';
|
||||
import 'me_journal.dart';
|
||||
@@ -14,8 +15,11 @@ class MeChatCreation {
|
||||
/// Selected agent public key.
|
||||
final String agent;
|
||||
|
||||
/// Optional Solo project, frozen with the rest of the creation intent.
|
||||
final String? project;
|
||||
|
||||
/// Create a new intent; persistence happens before [complete] performs writes.
|
||||
const MeChatCreation(this.id, this.name, this.agent);
|
||||
const MeChatCreation(this.id, this.name, this.agent, {this.project});
|
||||
|
||||
/// Account and community boundary for durable creation storage.
|
||||
static String storageKey(MeSync sync) =>
|
||||
@@ -31,6 +35,7 @@ class MeChatCreation {
|
||||
value['id'] as String,
|
||||
value['name'] as String,
|
||||
value['agent'] as String,
|
||||
project: value['project'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +46,8 @@ class MeChatCreation {
|
||||
if (previous != null &&
|
||||
(previous.id != id ||
|
||||
previous.name != name ||
|
||||
previous.agent != agent)) {
|
||||
previous.agent != agent ||
|
||||
previous.project != project)) {
|
||||
throw StateError('Resume the pending chat before starting another.');
|
||||
}
|
||||
if (previous != null) return;
|
||||
@@ -50,7 +56,12 @@ class MeChatCreation {
|
||||
storageKey(sync),
|
||||
nip44Encrypt(
|
||||
sync.key,
|
||||
jsonEncode({'id': id, 'name': name, 'agent': agent}),
|
||||
jsonEncode({
|
||||
'id': id,
|
||||
'name': name,
|
||||
'agent': agent,
|
||||
'project': project,
|
||||
}),
|
||||
),
|
||||
)) {
|
||||
throw StateError('Couldn’t save this chat on your phone. Try again.');
|
||||
@@ -63,7 +74,10 @@ class MeChatCreation {
|
||||
Future<void> discard(MeSync sync) async {
|
||||
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
||||
final current = pending(sync);
|
||||
if (current?.id != id || current?.name != name || current?.agent != agent) {
|
||||
if (current?.id != id ||
|
||||
current?.name != name ||
|
||||
current?.agent != agent ||
|
||||
current?.project != project) {
|
||||
throw StateError('The unfinished chat changed. Review it again.');
|
||||
}
|
||||
await clearMeJournal(
|
||||
|
||||
@@ -73,7 +73,11 @@ void main() {
|
||||
sync = MeSync(relay, config, prefs: prefs, connected: false);
|
||||
container.invalidate(meSyncProvider);
|
||||
}
|
||||
return {'name': 'Saved chat', 'assistant': 'agent'};
|
||||
return {
|
||||
'name': 'Saved chat',
|
||||
'assistant': 'agent',
|
||||
'project': 'mobile',
|
||||
};
|
||||
});
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
@@ -125,6 +129,7 @@ void main() {
|
||||
);
|
||||
expect(saved!.name, 'Saved chat');
|
||||
expect(saved.agent, 'agent');
|
||||
expect(saved.project, 'mobile');
|
||||
expect(
|
||||
prefs.getString(MeChatCreation.storageKey(sync)),
|
||||
isNot(contains('Saved chat')),
|
||||
|
||||
@@ -201,7 +201,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'footer new chat action opens both fields in one dismissible sheet',
|
||||
'footer new chat offers project selection without losing its name',
|
||||
(tester) async {
|
||||
final action = ValueNotifier<VoidCallback?>(null);
|
||||
addTearDown(action.dispose);
|
||||
@@ -220,7 +220,17 @@ void main() {
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
meGroupsProvider.overrideWith(
|
||||
(ref) => Stream.value(const MeGroups()),
|
||||
(ref) => Stream.value(
|
||||
const MeGroups(
|
||||
groups: [
|
||||
{
|
||||
'id': 'mobile',
|
||||
'name': 'Mobile app',
|
||||
'defaultTemplateId': '',
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
meSyncProvider.overrideWithValue(sync),
|
||||
channelsProvider.overrideWith(FakeChannels.new),
|
||||
@@ -250,6 +260,16 @@ void main() {
|
||||
find.byKey(const ValueKey('new-session-name')),
|
||||
'Design session',
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('new-session-project')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Mobile app').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Design session'), findsOneWidget);
|
||||
expect(find.text('Mobile app'), findsWidgets);
|
||||
await tester.tap(find.byKey(const ValueKey('new-session-project')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('No project').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Create chat'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Enter a name and choose an agent.'), findsOneWidget);
|
||||
@@ -257,7 +277,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('iOS new chat uses the native profile form with both fields', (
|
||||
testWidgets('iOS new chat uses the native profile form with project choices', (
|
||||
tester,
|
||||
) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
@@ -292,7 +312,17 @@ void main() {
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
meGroupsProvider.overrideWith(
|
||||
(ref) => Stream.value(const MeGroups()),
|
||||
(ref) => Stream.value(
|
||||
const MeGroups(
|
||||
groups: [
|
||||
{
|
||||
'id': 'mobile',
|
||||
'name': 'Mobile app',
|
||||
'defaultTemplateId': '',
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
meSyncProvider.overrideWithValue(sync),
|
||||
channelsProvider.overrideWith(FakeChannels.new),
|
||||
@@ -318,6 +348,10 @@ void main() {
|
||||
expect(args['title'], 'New chat');
|
||||
expect(args['placeholder'], 'Chat name');
|
||||
expect(args['assistants'], isEmpty);
|
||||
expect(args['projects'], [
|
||||
{'id': 'mobile', 'name': 'Mobile app'},
|
||||
]);
|
||||
expect(args['selectedProject'], isNull);
|
||||
expect(args['containerCornerRadius'], Radii.container);
|
||||
expect(find.byType(BottomSheet), findsNothing);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
@@ -363,6 +397,7 @@ void main() {
|
||||
'same-id',
|
||||
'Unfinished chat',
|
||||
'agent',
|
||||
project: 'mobile',
|
||||
).save(sync);
|
||||
final restarted = MeSync(
|
||||
RelaySessionNotifier(),
|
||||
@@ -401,6 +436,7 @@ void main() {
|
||||
expect(args['title'], 'New chat');
|
||||
expect(args['initialValue'], resume ? 'Unfinished chat' : '');
|
||||
expect(args['selectedAssistant'], resume ? 'agent' : isNull);
|
||||
expect(args['selectedProject'], resume ? 'mobile' : isNull);
|
||||
expect(args['fieldsLocked'], resume);
|
||||
expect(args['error'], resume ? contains('Resume') : isNull);
|
||||
expect(
|
||||
|
||||
@@ -79,7 +79,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
MeSync service() => MeSync(RelaySessionNotifier(), config, prefs: prefs);
|
||||
const intent = MeChatCreation('same-id', 'Private name', 'agent');
|
||||
const intent = MeChatCreation(
|
||||
'same-id',
|
||||
'Private name',
|
||||
'agent',
|
||||
project: 'mobile',
|
||||
);
|
||||
var created = false, member = false, placed = false, crash = true;
|
||||
final writes = [0, 0, 0];
|
||||
void boundary(int point) {
|
||||
@@ -118,6 +123,16 @@ void main() {
|
||||
final pending = MeChatCreation.pending(restarted)!;
|
||||
expect(pending.name, intent.name);
|
||||
expect(pending.agent, intent.agent);
|
||||
expect(pending.project, intent.project);
|
||||
await expectLater(
|
||||
MeChatCreation(
|
||||
intent.id,
|
||||
intent.name,
|
||||
intent.agent,
|
||||
project: 'other',
|
||||
).save(restarted),
|
||||
throwsStateError,
|
||||
);
|
||||
expect(
|
||||
prefs.getString(MeChatCreation.storageKey(restarted)),
|
||||
isNot(contains(intent.name)),
|
||||
|
||||
Reference in New Issue
Block a user