feat(app): the operator's overview of screens, grants to several people at once with one budget each, assistants by provider

Halkyon Learn's ADR 0021, D57 to D63, on halkyon_base's budgets and
effort:

- The operator's screen is an overview (Waiting, Operators, Accounts,
  Assistants, each with its count) whose rows open screens of their own,
  read 50 at a time as they scroll (OperatorListCubit, OperatorPage), so
  hundreds of accounts push nothing else down (D62).
- A handle is picked among the accounts there are (AccountSuggestionField):
  appointing an operator and granting take no other (D61).
- AssistantGrantsScreen replaces AssistantGrantsCard: the assistants set
  up by provider, with their efforts and prices; the people granted, found
  by their handle's start; one sheet granting several assistants, a
  provider's at once, to several people, each with one budget in money and
  tokens a day and a month, or changing one person's (D58, D59).
- Assistants are grouped by provider wherever they are listed, each under
  its name with a dot in its colour (ProviderLabel, D60).
- AssistantsCard shows what is left of the person's budget for all their
  assistants together; the months of the figures run oldest first (D57).
- AssistantChoice says the efforts a question may ask at and the ceiling
  on output each asks for (D63); a person's own key gets none and keeps
  4,096 tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-26 16:05:49 +01:00
commit cc87120bc8
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 5348 additions and 2709 deletions

View file

@ -1,327 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// The operator's screen's Assistants card (Halkyon Learn's ADR 0021,
/// points 2, 6 and 8): grants by handle with their limits, granting,
/// changing and taking back, and everyone's use by month.
void main() {
late TestAppDependencies dependencies;
late FakeAssistantOperatorGateway gateway;
const claude = SetUpAssistant(
id: 'claude',
name: 'Claude',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-sonnet-5-5',
priceInput: 3,
priceOutput: 15,
);
const local = SetUpAssistant(
id: 'local',
name: 'Local',
protocol: 'openai',
host: 'models.example.org',
model: 'qwen',
);
setUp(() {
dependencies = TestAppDependencies();
dependencies.operatorGateway
..operates = true
..add(
OperatedAccount(
id: 'anna',
handle: 'anna',
since: DateTime.utc(2026, 10),
isOperator: true,
),
)
..add(
OperatedAccount(
id: 'ben',
handle: 'ben',
since: DateTime.utc(2026, 10, 2),
),
);
gateway = dependencies.assistantOperatorGateway
..set = const AssistantsSetUp(
assistants: [claude, local],
currency: 'EUR',
)
..accounts.addAll({'anna': 'anna', 'ben': 'ben'});
});
Future<void> show(WidgetTester tester) async {
tester.view.physicalSize = const Size(412, 2400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
testApp(const InstanceOperatorScreen(), dependencies: dependencies),
);
await tester.pumpAndSettle();
}
/// [finder]'s match in the sheet, where the screen behind has its own.
Finder inSheet(Finder finder) =>
find.descendant(of: find.byType(SheetScaffold), matching: finder);
Future<void> tap(WidgetTester tester, Finder finder) async {
await tester.ensureVisible(finder);
await tester.tap(finder);
await tester.pumpAndSettle();
}
testWidgets('shows nothing of assistants where none is set and none '
'granted', (tester) async {
gateway.set = const AssistantsSetUp();
await show(tester);
expect(find.text('Assistants'), findsNothing);
expect(find.text('Grant an assistant'), findsNothing);
});
testWidgets("shows nothing of them where they can't be read", (
tester,
) async {
gateway.failing = const OperatorFailed(OperatorProblem.notAllowed);
await show(tester);
expect(find.text('Assistants'), findsNothing);
expect(find.text('Find an account'), findsOneWidget);
});
testWidgets('lists each grant with its assistant, limits and whether its '
'person agreed', (tester) async {
gateway.granted.addAll([
AssistantGrant(
account: 'ben',
handle: 'ben',
assistant: 'claude',
grantedAt: DateTime.utc(2026, 10, 20),
unit: AssistantLimitUnit.money,
dailyLimit: 1000000,
monthlyLimit: 10000000,
),
AssistantGrant(
account: 'anna',
handle: 'anna',
assistant: 'local',
grantedAt: DateTime.utc(2026, 10, 20),
agreedAt: DateTime.utc(2026, 10, 21),
),
]);
await show(tester);
expect(find.text('Assistants'), findsOneWidget);
expect(
find.text('Claude · €1.00 a day · €10.00 a month · Not agreed yet'),
findsOneWidget,
);
expect(find.text('Local · No limits · Agreed'), findsOneWidget);
});
testWidgets("grants by handle, in its pricing's unit, and lists it", (
tester,
) async {
await show(tester);
await tap(tester, find.text('Grant an assistant'));
await tester.enterText(
inSheet(find.widgetWithText(TextField, 'Handle')),
'ben',
);
await tester.enterText(
find.widgetWithText(TextField, 'Limit a day'),
'2,5',
);
await tap(tester, find.widgetWithText(FilledButton, 'Grant'));
final grant = gateway.granted.single;
expect(grant.account, 'ben');
expect(grant.dailyLimit, 2500000);
expect(grant.monthlyLimit, isNull);
expect(find.text('Claude · €2.50 a day · Not agreed yet'), findsOneWidget);
await tap(tester, find.text('Grant an assistant'));
await tap(tester, find.text('Local'));
await tester.enterText(
inSheet(find.widgetWithText(TextField, 'Handle')),
'@Ben',
);
await tester.enterText(
find.widgetWithText(TextField, 'Limit a month'),
'1.000.000',
);
await tap(tester, find.widgetWithText(FilledButton, 'Grant'));
expect(gateway.granted.last.assistant, 'local');
expect(gateway.granted.last.monthlyLimit, 1000000);
expect(
find.text('Local · 1,000,000 tokens a month · Not agreed yet'),
findsOneWidget,
);
});
testWidgets("a limit that isn't one is said so, and nothing is sent", (
tester,
) async {
await show(tester);
await tap(tester, find.text('Grant an assistant'));
await tester.enterText(
inSheet(find.widgetWithText(TextField, 'Handle')),
'ben',
);
await tester.enterText(
find.widgetWithText(TextField, 'Limit a month'),
'ten',
);
await tap(tester, find.widgetWithText(FilledButton, 'Grant'));
expect(find.text('Not a number this takes'), findsOneWidget);
expect(gateway.calls.where((call) => call.startsWith('grant ')), isEmpty);
});
testWidgets('a handle nobody has is said so in the sheet, and nothing is '
'granted', (tester) async {
await show(tester);
await tap(tester, find.text('Grant an assistant'));
await tester.enterText(
inSheet(find.widgetWithText(TextField, 'Handle')),
'zed',
);
await tap(tester, find.widgetWithText(FilledButton, 'Grant'));
expect(find.text('No account has this handle.'), findsOneWidget);
expect(gateway.granted, isEmpty);
});
testWidgets('an account found offers its grant, its handle filled in, an '
'operator too', (tester) async {
await show(tester);
await tester.enterText(find.widgetWithText(TextField, 'Handle'), 'an');
await tester.pumpAndSettle();
await tap(tester, find.byTooltip('Show menu'));
expect(find.text('Suspend'), findsNothing, reason: 'an operator');
await tap(tester, find.text('Grant an assistant').last);
expect(
find.descendant(
of: find.byType(SheetScaffold),
matching: find.text('anna'),
),
findsOneWidget,
);
await tap(tester, find.widgetWithText(FilledButton, 'Grant'));
expect(gateway.granted.single.account, 'anna');
});
testWidgets('changes the limits; taking it back asks first', (
tester,
) async {
gateway.granted.add(
AssistantGrant(
account: 'ben',
handle: 'ben',
assistant: 'claude',
grantedAt: DateTime.utc(2026, 10, 20),
unit: AssistantLimitUnit.money,
dailyLimit: 1000000,
),
);
await show(tester);
await tap(tester, find.byTooltip('Show menu').last);
await tap(tester, find.text('Change limits'));
expect(find.widgetWithText(TextField, '1'), findsOneWidget);
await tester.enterText(find.widgetWithText(TextField, 'Limit a day'), '');
await tap(tester, find.widgetWithText(FilledButton, 'Save'));
expect(gateway.granted.single.dailyLimit, isNull);
expect(find.text('Claude · No limits · Not agreed yet'), findsOneWidget);
await tap(tester, find.byTooltip('Show menu').last);
await tap(tester, find.text('Take back').last);
expect(find.text('Take back Claude?'), findsOneWidget);
await tap(tester, find.widgetWithText(TextButton, 'Cancel'));
expect(gateway.granted, hasLength(1));
await tap(tester, find.byTooltip('Show menu').last);
await tap(tester, find.text('Take back').last);
await tap(tester, find.widgetWithText(FilledButton, 'Take back'));
expect(gateway.granted, isEmpty);
expect(find.text('Nobody has an assistant yet.'), findsOneWidget);
});
testWidgets('a grant of an assistant no longer set says so, and can only '
'be taken back', (tester) async {
gateway.granted.add(
AssistantGrant(
account: 'ben',
handle: 'ben',
assistant: 'gpt',
assistantSet: false,
grantedAt: DateTime.utc(2026, 10, 20),
),
);
await show(tester);
expect(
find.text('gpt · No longer set up · Not agreed yet'),
findsOneWidget,
);
await tap(tester, find.byTooltip('Show menu').last);
expect(find.text('Change limits'), findsNothing);
expect(
find.text('Take back'),
findsNWidgets(2),
reason: "the operators' card's own, and the menu's",
);
});
testWidgets("everyone's use by UTC month, never what was asked", (
tester,
) async {
gateway.use[utcMonthOf(DateTime.now())] = [
AssistantUse(
assistant: 'claude',
month: utcMonthOf(DateTime.now()),
requests: 3,
inputTokens: 1200,
outputTokens: 300,
costMicros: 8100,
account: 'ben',
handle: 'ben',
),
];
await show(tester);
await tap(tester, find.text('Use by month'));
expect(find.text('Use of assistants'), findsOneWidget);
expect(find.text('@ben'), findsOneWidget);
expect(
find.text('Claude · 3 requests · 1,500 tokens · €0.01'),
findsOneWidget,
);
expect(find.byType(SegmentedButton<DateTime>), findsOneWidget);
final before = utcMonthOf(DateTime.now());
await tap(
tester,
find
.descendant(
of: find.byType(SegmentedButton<DateTime>),
matching: find.byType(Text),
)
.last,
);
expect(gateway.calls.last, isNot('figures ${before.toIso8601String()}'));
expect(find.text('Nothing used in this month.'), findsOneWidget);
});
}

View file

@ -0,0 +1,427 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_llm/halkyon_llm.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// The operator's assistants (Halkyon Learn's ADR 0021, points 2, 6 and 8,
/// D57 to D63): those set up by provider, granted to several people at
/// once with one budget each, by handles suggested as they are typed; the
/// people granted, changed and taken back; and everyone's use by month,
/// the oldest on the left.
void main() {
late TestAppDependencies dependencies;
late FakeAssistantOperatorGateway gateway;
const claude = SetUpAssistant(
id: 'claude',
name: 'Claude',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-sonnet-5-5',
priceInput: 3,
priceOutput: 15,
effort: LlmEffort.medium,
maxEffort: LlmEffort.high,
);
const opus = SetUpAssistant(
id: 'opus',
name: 'Opus',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-opus-5-5',
priceInput: 5,
priceOutput: 25,
);
const local = SetUpAssistant(
id: 'local',
name: 'Local',
protocol: 'openai',
host: 'models.example.org',
model: 'qwen',
);
setUp(() {
dependencies = TestAppDependencies();
dependencies.operatorGateway
..operates = true
..add(
OperatedAccount(
id: 'anna',
handle: 'anna',
since: DateTime.utc(2026, 10),
isOperator: true,
),
)
..add(
OperatedAccount(
id: 'ben',
handle: 'ben',
since: DateTime.utc(2026, 10, 2),
),
)
..add(
OperatedAccount(
id: 'bob',
handle: 'bob',
since: DateTime.utc(2026, 10, 3),
waitingSince: DateTime.utc(2026, 10, 3),
),
);
gateway = dependencies.assistantOperatorGateway
..set = const AssistantsSetUp(
assistants: [claude, opus, local],
currency: 'EUR',
)
..accounts.addAll({'anna': 'anna', 'ben': 'ben'});
});
/// The operator's screen, its assistants opened.
Future<void> show(WidgetTester tester) async {
tester.view.physicalSize = const Size(412, 2400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
testApp(const InstanceOperatorScreen(), dependencies: dependencies),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Assistants'));
await tester.pumpAndSettle();
}
/// [finder]'s match in the sheet, where the screen behind has its own.
Finder inSheet(Finder finder) =>
find.descendant(of: find.byType(SheetScaffold), matching: finder);
Future<void> tap(WidgetTester tester, Finder finder) async {
await tester.ensureVisible(finder);
await tester.tap(finder);
await tester.pumpAndSettle();
}
/// Adds the person [typed] suggests in the sheet.
Future<void> addPerson(WidgetTester tester, String typed) async {
await tester.enterText(inSheet(find.byType(TextField)).first, typed);
await tester.pumpAndSettle();
await tap(tester, inSheet(find.widgetWithText(ListTile, '@$typed')));
}
Future<void> type(WidgetTester tester, String field, String text) async {
await tester.enterText(
inSheet(find.widgetWithText(TextField, field)),
text,
);
await tester.pumpAndSettle();
}
Finder saveButton() => inSheet(find.byType(FilledButton));
testWidgets('lists those set up by provider, with their efforts and '
'whether they have prices', (tester) async {
await show(tester);
expect(find.text('Anthropic'), findsOneWidget);
expect(find.text('models.example.org'), findsOneWidget);
expect(
find.text(
'claude-sonnet-5-5 at api.anthropic.com · Effort medium, up to high',
),
findsOneWidget,
);
expect(find.text('claude-opus-5-5 at api.anthropic.com'), findsOneWidget);
expect(
find.text(
'qwen at models.example.org · No prices: counts its tokens only',
),
findsOneWidget,
);
expect(find.text('Nobody has an assistant yet.'), findsOneWidget);
});
testWidgets('grants several people a whole provider at once, with one '
'budget each, and lists them', (tester) async {
await show(tester);
await tap(tester, find.text('Grant assistants'));
expect(tester.widget<FilledButton>(saveButton()).onPressed, isNull);
await addPerson(tester, 'anna');
await addPerson(tester, 'ben');
expect(inSheet(find.widgetWithText(InputChip, '@anna')), findsOneWidget);
expect(inSheet(find.widgetWithText(InputChip, '@ben')), findsOneWidget);
await tap(
tester,
inSheet(find.widgetWithText(CheckboxListTile, 'Anthropic')),
);
await type(tester, 'Money a day', '1,5');
await type(tester, 'Tokens a month', '2,000,000');
await tap(tester, saveButton());
for (final account in ['anna', 'ben']) {
final grantee = gateway.people[account]!;
expect(
[for (final grant in grantee.grants) grant.assistant],
[
'claude',
'opus',
],
);
expect(
grantee.budget,
const AssistantBudget(dailyCostMicros: 1500000, monthlyTokens: 2000000),
);
}
expect(find.byType(SheetScaffold), findsNothing);
expect(find.text('@ben'), findsOneWidget);
expect(
find.text(
'Claude, Opus\n'
'€1.50 of €1.50 left today\n'
'2,000,000 of 2,000,000 tokens left this month\n'
'2 not agreed to yet',
),
findsNWidgets(2),
);
await tester.pageBack();
await tester.pumpAndSettle();
expect(find.text('3 assistants · 2 people'), findsOneWidget);
});
testWidgets('suggests only handles there are of accounts approved '
'(D61)', (tester) async {
await show(tester);
await tap(tester, find.text('Grant assistants'));
await tester.enterText(inSheet(find.byType(TextField)).first, 'b');
await tester.pumpAndSettle();
expect(inSheet(find.text('@ben')), findsOneWidget);
expect(inSheet(find.text('@bob')), findsNothing, reason: 'bob waits');
await tester.enterText(inSheet(find.byType(TextField)).first, 'zed');
await tester.pumpAndSettle();
expect(
inSheet(find.text("No account's handle starts like this.")),
findsOneWidget,
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(inSheet(find.byType(InputChip)), findsNothing);
});
testWidgets('a handle typed whole is added by Enter', (tester) async {
await show(tester);
await tap(tester, find.text('Grant assistants'));
await tester.enterText(inSheet(find.byType(TextField)).first, '@Ben');
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(inSheet(find.widgetWithText(InputChip, '@ben')), findsOneWidget);
});
testWidgets('names whose budget a grant replaces before it does', (
tester,
) async {
gateway.people['ben'] = AssistantGrantee(
account: 'ben',
handle: 'ben',
grants: [AssistantGrant(assistant: 'local', grantedAt: DateTime(2026))],
budget: const AssistantBudget(dailyCostMicros: 1000000),
);
await show(tester);
await tap(tester, find.text('Grant assistants'));
await addPerson(tester, 'anna');
expect(inSheet(find.textContaining('replaces')), findsNothing);
await addPerson(tester, 'ben');
expect(
inSheet(find.text('Granting replaces the budget this person has:')),
findsOneWidget,
);
expect(inSheet(find.text('@ben: €1.00 a day')), findsOneWidget);
});
testWidgets("a limit that isn't one is said so, and nothing is sent", (
tester,
) async {
await show(tester);
await tap(tester, find.text('Grant assistants'));
await addPerson(tester, 'ben');
await tap(tester, inSheet(find.widgetWithText(CheckboxListTile, 'Local')));
await type(tester, 'Tokens a day', 'a lot');
await tap(tester, saveButton());
expect(inSheet(find.text('Not a number this takes')), findsOneWidget);
expect(gateway.people, isEmpty);
});
testWidgets("changes a person's assistants and budget, and takes every "
'one back after asking', (tester) async {
gateway.people['ben'] = AssistantGrantee(
account: 'ben',
handle: 'ben',
grants: [
AssistantGrant(
assistant: 'claude',
grantedAt: DateTime(2026),
agreedAt: DateTime(2026),
),
],
budget: const AssistantBudget(dailyTokens: 1000),
);
await show(tester);
await tap(tester, find.text('@ben'));
expect(inSheet(find.text('@ben')), findsOneWidget);
expect(
tester
.widget<CheckboxListTile>(
inSheet(find.widgetWithText(CheckboxListTile, 'Claude')),
)
.value,
isTrue,
);
expect(
tester
.widget<TextField>(
inSheet(find.widgetWithText(TextField, 'Tokens a day')),
)
.controller!
.text,
'1000',
);
await tap(tester, inSheet(find.widgetWithText(CheckboxListTile, 'Claude')));
await tap(tester, inSheet(find.widgetWithText(CheckboxListTile, 'Local')));
await type(tester, 'Tokens a day', '');
await tap(tester, saveButton());
expect(gateway.calls, contains('change ben local'));
expect(find.text('Local\nNo limits\n1 not agreed to yet'), findsOneWidget);
await tap(tester, find.byTooltip('Show menu'));
await tap(tester, find.text('Take back').last);
expect(find.text('Take back every assistant?'), findsOneWidget);
await tap(tester, find.widgetWithText(FilledButton, 'Take back'));
expect(gateway.people, isEmpty);
expect(find.text('Nobody has an assistant yet.'), findsOneWidget);
});
testWidgets('one granted an assistant no longer set says so, and changing '
'takes it back', (tester) async {
gateway.people['ben'] = AssistantGrantee(
account: 'ben',
handle: 'ben',
grants: [
AssistantGrant(assistant: 'claude', grantedAt: DateTime(2026)),
AssistantGrant(
assistant: 'gpt',
assistantSet: false,
grantedAt: DateTime(2026),
),
],
);
await show(tester);
expect(find.textContaining('gpt (no longer set up)'), findsOneWidget);
await tap(tester, find.text('@ben'));
expect(
inSheet(find.text('Saving takes back what is no longer set up: gpt.')),
findsOneWidget,
);
});
testWidgets('finds the people granted by the start of their handle', (
tester,
) async {
for (final account in ['anna', 'ben']) {
gateway.people[account] = AssistantGrantee(
account: account,
handle: account,
grants: [AssistantGrant(assistant: 'local', grantedAt: DateTime(2026))],
);
}
await show(tester);
expect(find.text('@anna'), findsOneWidget);
await tester.enterText(find.widgetWithText(TextField, 'People'), 'be');
await tester.pumpAndSettle();
expect(find.text('@anna'), findsNothing);
expect(find.text('@ben'), findsOneWidget);
await tester.enterText(find.widgetWithText(TextField, 'People'), 'zed');
await tester.pumpAndSettle();
expect(
find.text('Nobody with an assistant has a handle starting like this.'),
findsOneWidget,
);
});
testWidgets('an account found offers its grant, the person in the sheet '
'already', (tester) async {
tester.view.physicalSize = const Size(412, 2400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
testApp(const InstanceOperatorScreen(), dependencies: dependencies),
);
await tester.pumpAndSettle();
await tap(tester, find.text('Accounts'));
await tester.enterText(find.byType(TextField), 'ben');
await tester.pumpAndSettle();
await tap(tester, find.byTooltip('Show menu'));
await tap(tester, find.text('Grant assistants'));
expect(inSheet(find.widgetWithText(InputChip, '@ben')), findsOneWidget);
});
testWidgets("everyone's use by UTC month, the oldest on the left, never "
'what was asked', (tester) async {
final now = utcMonthOf(DateTime.now());
gateway.use[now] = [
AssistantUse(
assistant: 'claude',
month: now,
requests: 3,
inputTokens: 1200,
outputTokens: 300,
costMicros: 8100,
account: 'ben',
handle: 'ben',
),
];
await show(tester);
await tap(tester, find.text('Use by month'));
expect(find.text('Use of assistants'), findsOneWidget);
expect(find.text('@ben'), findsOneWidget);
expect(
find.text('Claude · 3 requests · 1,500 tokens · €0.01'),
findsOneWidget,
);
final months = find.descendant(
of: find.byType(SegmentedButton<DateTime>),
matching: find.byType(Text),
);
expect(
tester
.widget<SegmentedButton<DateTime>>(
find.byType(SegmentedButton<DateTime>),
)
.selected,
{now},
);
await tap(tester, months.first);
expect(
gateway.calls.last,
'figures ${DateTime.utc(now.year, now.month - 2).toIso8601String()}',
);
expect(find.text('Nothing used in this month.'), findsOneWidget);
});
}

View file

@ -6,8 +6,9 @@ import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// An app's settings' Assistants card (Halkyon Learn's ADR 0021, points 6,
/// 9 and 10): what is granted and left, agreeing to what goes where once,
/// the person's use, and assistants with their own key where allowed.
/// 9 and 10, D58, D60): what is granted, by provider, and left of the
/// budget for them all, agreeing to what goes where once, the person's
/// use, and assistants with their own key where allowed.
void main() {
late TestAppDependencies dependencies;
late FakeAssistantGateway gateway;
@ -20,9 +21,6 @@ void main() {
host: 'api.anthropic.com',
model: 'claude-sonnet-5-5',
images: true,
unit: AssistantLimitUnit.money,
dailyLimit: 1000000,
usedToday: 8100,
);
setUp(() {
@ -32,6 +30,9 @@ void main() {
assistants: [claude],
ownKeys: true,
currency: 'EUR',
budget: AssistantBudget(dailyCostMicros: 1000000),
usedToday: BudgetUse(costMicros: 8100, tokens: 900),
usedThisMonth: BudgetUse(costMicros: 8100, tokens: 900),
);
store = MemoryOwnAssistantStore();
});
@ -77,48 +78,89 @@ void main() {
expect(find.text('Assistants'), findsNothing);
});
testWidgets('shows who answers and what is left of its limits', (
tester,
) async {
testWidgets('shows who answers, and what is left of the budget for them '
'all', (tester) async {
await show(tester);
expect(find.text('Assistants'), findsOneWidget);
expect(find.text('Your budget, for all of them'), findsOneWidget);
expect(find.text('€0.99 of €1.00 left today'), findsOneWidget);
expect(find.text('Anthropic'), findsOneWidget);
expect(find.text('Claude'), findsOneWidget);
expect(
find.text(
'claude-sonnet-5-5 at api.anthropic.com\n€0.99 of €1.00 left today\n'
'Read and agree',
),
find.text('claude-sonnet-5-5 at api.anthropic.com\nRead and agree'),
findsOneWidget,
);
expect(find.text('Your use'), findsOneWidget);
});
testWidgets('a limit in tokens names them once', (tester) async {
testWidgets('a budget in tokens names them once', (tester) async {
gateway.offer = const AssistantsOffer(
assistants: [
OfferedAssistant(
id: 'local',
name: 'Local',
protocol: 'openai',
host: 'models.example.org',
model: 'qwen',
monthlyLimit: 2000000,
usedThisMonth: 350000,
),
],
assistants: [claude],
budget: AssistantBudget(dailyTokens: 100000, monthlyTokens: 2000000),
usedToday: BudgetUse(tokens: 350000),
usedThisMonth: BudgetUse(tokens: 350000),
);
await show(tester);
expect(
find.text(
'qwen at models.example.org\n1,650,000 of 2,000,000 tokens left this '
'month\nRead and agree',
'0 of 100,000 tokens left today\n'
'1,650,000 of 2,000,000 tokens left this month',
),
findsOneWidget,
);
});
testWidgets('a budget without limits says so', (tester) async {
gateway.offer = const AssistantsOffer(assistants: [claude]);
await show(tester);
expect(find.text('No limits'), findsOneWidget);
});
testWidgets('groups the assistants by provider, in the order the operator '
'set them', (tester) async {
gateway.offer = const AssistantsOffer(
assistants: [
claude,
OfferedAssistant(
id: 'gemini',
name: 'Gemini',
protocol: 'openai',
host: 'generativelanguage.googleapis.com',
model: 'gemini-3-pro',
),
OfferedAssistant(
id: 'opus',
name: 'Opus',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-opus-5-5',
),
],
);
await show(tester);
final labels = [
for (final text in tester.widgetList<Text>(
find.descendant(
of: find.byType(AssistantsCard),
matching: find.byType(Text),
),
))
if (const {
'Anthropic',
'Google',
'Claude',
'Opus',
'Gemini',
}.contains(text.data))
text.data,
];
expect(labels, ['Anthropic', 'Claude', 'Opus', 'Google', 'Gemini']);
});
testWidgets('says what goes where before agreeing, and agrees only when '
'asked to (D40)', (tester) async {
await show(tester);