feat(app,learn,todo): the assistants in Settings and on the operator's screen
ADR 0021, step 2's app half. halkyon_app asks the module's assistant endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway) and a person's own provider directly (halkyon_llm), behind AssistantRepository: an AssistantChoice is a GrantedAssistant, asked through the instance, which counts it, or a PersonalAssistant with the person's own key, which the instance never sees. assistant_messages.dart turns a conversation into the endpoint's models and back. AssistantsCard, for an app's settings: what is granted and left of its limits, agreed to once on a sheet saying what goes where, the person's use by UTC month, the instance's privacy page, and their own assistants where the operator allows own keys (kept ones stay listed, unasked, where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants by handle with their limits in the pricing's unit, changed and taken back after asking, everyone's use by month; a found account's menu grants too. Nothing shows where nothing is offered or can be read. package:halkyon_app/assistant_models.dart holds the models in pure Dart. Learn keeps own assistants in the person's settings (ownAssistants, schema 12, SettingsOwnAssistants); SettingsRepository.update refuses (OwnAssistantRefused) to write a key where the settings would not be sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly. Todo wires the gateways; a member offers no assistant. No feature asks an assistant yet (step 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1e5d7040fd
commit
36899cdc3b
2 changed files with 7505 additions and 46 deletions
|
|
@ -0,0 +1,327 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
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';
|
||||
|
||||
/// 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.
|
||||
void main() {
|
||||
late TestAppDependencies dependencies;
|
||||
late FakeAssistantGateway gateway;
|
||||
late MemoryOwnAssistantStore store;
|
||||
|
||||
const claude = OfferedAssistant(
|
||||
id: 'claude',
|
||||
name: 'Claude',
|
||||
protocol: 'anthropic',
|
||||
host: 'api.anthropic.com',
|
||||
model: 'claude-sonnet-5-5',
|
||||
images: true,
|
||||
unit: AssistantLimitUnit.money,
|
||||
dailyLimit: 1000000,
|
||||
usedToday: 8100,
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
dependencies = TestAppDependencies();
|
||||
gateway = dependencies.assistantGateway
|
||||
..offer = const AssistantsOffer(
|
||||
assistants: [claude],
|
||||
ownKeys: true,
|
||||
currency: 'EUR',
|
||||
);
|
||||
store = MemoryOwnAssistantStore();
|
||||
});
|
||||
|
||||
Future<void> show(WidgetTester tester, {bool keepsOwn = true}) async {
|
||||
tester.view.physicalSize = const Size(412, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: AssistantsCard(ownAssistants: keepsOwn ? store : null),
|
||||
),
|
||||
),
|
||||
dependencies: dependencies,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
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 where nothing is offered and own keys are not '
|
||||
'allowed', (tester) async {
|
||||
gateway.offer = AssistantsOffer.none;
|
||||
await show(tester);
|
||||
|
||||
expect(find.text('Assistants'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("shows nothing where the offer can't be read", (tester) async {
|
||||
gateway.failing = const AssistantFailed(AssistantProblem.failed);
|
||||
await show(tester);
|
||||
|
||||
expect(find.text('Assistants'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows who answers and what is left of its limits', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester);
|
||||
|
||||
expect(find.text('Assistants'), 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',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Your use'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a limit 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
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',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('says what goes where before agreeing, and agrees only when '
|
||||
'asked to (D40)', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Claude'));
|
||||
expect(find.text('Before you ask Claude'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'It answers with the model claude-sonnet-5-5 at '
|
||||
'api.anthropic.com.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('It reads the images you add.'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'Whoever runs test.example.org sees how much you use it (requests, '
|
||||
'tokens, cost), never what you ask.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(gateway.calls, isNot(contains('agree claude')));
|
||||
|
||||
await tap(tester, find.widgetWithText(FilledButton, 'Agree'));
|
||||
|
||||
expect(gateway.calls, contains('agree claude'));
|
||||
expect(find.text('Before you ask Claude'), findsNothing);
|
||||
expect(find.textContaining('Read and agree'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('once agreed, the sheet says since when and asks nothing', (
|
||||
tester,
|
||||
) async {
|
||||
gateway.offer = AssistantsOffer(
|
||||
assistants: [
|
||||
OfferedAssistant(
|
||||
id: claude.id,
|
||||
name: claude.name,
|
||||
protocol: claude.protocol,
|
||||
host: claude.host,
|
||||
model: claude.model,
|
||||
agreedAt: DateTime.utc(2026, 10, 25, 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Claude'));
|
||||
|
||||
expect(find.text('You agreed on Oct 25, 2026.'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Agree'), findsNothing);
|
||||
expect(find.text('It reads the images you add.'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a refused agreement is said so', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Claude'));
|
||||
gateway.failing = const AssistantFailed(AssistantProblem.notGranted);
|
||||
await tap(tester, find.widgetWithText(FilledButton, 'Agree'));
|
||||
|
||||
expect(
|
||||
find.text("This assistant isn't granted to you any more."),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("the person's use, by UTC month", (tester) async {
|
||||
final month = utcMonthOf(DateTime.now());
|
||||
gateway.use = [
|
||||
AssistantUse(
|
||||
assistant: 'claude',
|
||||
month: month,
|
||||
requests: 1,
|
||||
inputTokens: 900,
|
||||
outputTokens: 100,
|
||||
costMicros: 4500,
|
||||
),
|
||||
];
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Your use'));
|
||||
|
||||
expect(inSheet(find.text('Claude')), findsOneWidget);
|
||||
expect(
|
||||
inSheet(find.text('1 request · 1,000 tokens · €0.00')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('adds an assistant with an own key, kept in the settings', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Add an assistant with your own key'));
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Name')),
|
||||
'My GPT',
|
||||
);
|
||||
await tap(tester, find.text('OpenAI Responses'));
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Model')),
|
||||
'gpt-5',
|
||||
);
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Key')),
|
||||
'sk-own',
|
||||
);
|
||||
await tap(tester, inSheet(find.widgetWithText(FilledButton, 'Save')));
|
||||
|
||||
final own = store.assistants.single;
|
||||
expect(own.name, 'My GPT');
|
||||
expect(own.protocol, LlmProtocol.openaiResponses);
|
||||
expect(own.baseUrl, isNull);
|
||||
expect(own.model, 'gpt-5');
|
||||
expect(own.apiKey, 'sk-own');
|
||||
expect(find.text('My GPT'), findsOneWidget);
|
||||
expect(find.text('gpt-5 at api.openai.com'), findsOneWidget);
|
||||
expect(find.text('sk-own'), findsNothing, reason: 'never shown');
|
||||
});
|
||||
|
||||
testWidgets("an address that isn't https is refused before anything is "
|
||||
'kept', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Add an assistant with your own key'));
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Name')),
|
||||
'Remote',
|
||||
);
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Address')),
|
||||
'http://models.example.org/v1',
|
||||
);
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Model')),
|
||||
'qwen',
|
||||
);
|
||||
await tap(tester, inSheet(find.widgetWithText(FilledButton, 'Save')));
|
||||
|
||||
expect(
|
||||
find.text('Only https, or http on your own network'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(store.assistants, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a key isn't kept where this device can't seal the "
|
||||
'settings', (tester) async {
|
||||
store.unsealed = true;
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('Add an assistant with your own key'));
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Name')),
|
||||
'Mine',
|
||||
);
|
||||
await tester.enterText(
|
||||
inSheet(find.widgetWithText(TextField, 'Model')),
|
||||
'claude-sonnet-5-5',
|
||||
);
|
||||
await tap(tester, inSheet(find.widgetWithText(FilledButton, 'Save')));
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
"The key wasn't kept: this device can't encrypt your settings yet. "
|
||||
"Open your account's keys here first.",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(store.assistants, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('removing an own assistant asks first', (tester) async {
|
||||
await store.save(
|
||||
const OwnAssistant(
|
||||
id: 'own-1',
|
||||
name: 'My GPT',
|
||||
protocol: LlmProtocol.openai,
|
||||
model: 'gpt-5',
|
||||
apiKey: 'sk-own',
|
||||
),
|
||||
);
|
||||
await show(tester);
|
||||
|
||||
await tap(tester, find.text('My GPT'));
|
||||
await tap(tester, find.text('Remove'));
|
||||
expect(find.text('Remove My GPT?'), findsOneWidget);
|
||||
await tap(tester, find.widgetWithText(FilledButton, 'Remove'));
|
||||
|
||||
expect(store.assistants, isEmpty);
|
||||
expect(find.text('My GPT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("where own keys aren't allowed, those kept are said to go "
|
||||
'unasked, and none can be added (D6)', (tester) async {
|
||||
gateway.offer = AssistantsOffer.none;
|
||||
await store.save(
|
||||
const OwnAssistant(
|
||||
id: 'own-1',
|
||||
name: 'My GPT',
|
||||
protocol: LlmProtocol.openai,
|
||||
model: 'gpt-5',
|
||||
apiKey: 'sk-own',
|
||||
),
|
||||
);
|
||||
await show(tester);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
"Whoever runs test.example.org doesn't allow own keys here, so the "
|
||||
"app doesn't ask these.",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Add an assistant with your own key'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an app keeping no own assistants offers none', (tester) async {
|
||||
await show(tester, keepsOwn: false);
|
||||
|
||||
expect(find.text('Claude'), findsOneWidget);
|
||||
expect(find.text('With your own key'), findsNothing);
|
||||
expect(find.text('Add an assistant with your own key'), findsNothing);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue