import 'package:clock/clock.dart'; 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'; // The person's assistants in an app's settings, the operator's grants and // the use by month (Halkyon Learn's ADR 0021, step 2). const _claude = OfferedAssistant( id: 'claude', name: 'Claude', protocol: 'anthropic', host: 'api.anthropic.com', model: 'claude-sonnet-5-5', ); const _own = OwnAssistant( id: 'own-1', name: 'My GPT', protocol: LlmProtocol.openai, model: 'gpt-5', apiKey: 'sk-own', ); const _setUp = AssistantsSetUp( assistants: [ SetUpAssistant( id: 'claude', name: 'Claude', protocol: 'anthropic', host: 'api.anthropic.com', model: 'claude-sonnet-5-5', priceInput: 3, priceOutput: 15, ), ], ); void main() { group('AssistantsCubit', () { late FakeAssistantGateway gateway; late AssistantRepository repository; late MemoryOwnAssistantStore store; late AssistantsCubit cubit; setUp(() { gateway = FakeAssistantGateway() ..offer = const AssistantsOffer(assistants: [_claude], ownKeys: true); repository = AssistantRepository(gateway, llm: FakeLlmClient()); store = MemoryOwnAssistantStore(); cubit = AssistantsCubit(repository, ownAssistants: store); addTearDown(() async { await cubit.close(); await repository.dispose(); }); }); test('reads what is offered, and the own assistants as they ' 'change', () async { await cubit.load(); expect(cubit.state.loading, isFalse); expect(cubit.state.offer.assistants, [_claude]); expect(cubit.state.showsAnything, isTrue); expect(await cubit.saveOwn(_own), OwnAssistantSaved.kept); await pumpEventQueue(); expect(cubit.state.own, [_own]); await cubit.removeOwn('own-1'); await pumpEventQueue(); expect(cubit.state.own, isEmpty); }); test('shows nothing where nothing is offered, own keys are not ' 'allowed, and none are kept', () async { gateway.offer = AssistantsOffer.none; await cubit.load(); expect(cubit.state.showsAnything, isFalse); }); test("a key isn't kept where the settings wouldn't be sealed", () async { store.unsealed = true; expect(await cubit.saveOwn(_own), OwnAssistantSaved.refused); expect(store.assistants, isEmpty); }); test('agreeing reads the offer anew', () async { await cubit.load(); await cubit.agree('claude'); expect(cubit.state.offer.assistants.single.agreed, isTrue); expect(cubit.state.agreeing, isNull); expect(cubit.state.problem, isNull); }); test('a refused agreement is the problem, the offer as it was', () async { await cubit.load(); gateway.failing = const AssistantFailed(AssistantProblem.notAllowed); await cubit.agree('claude'); expect( cubit.state.problem, const AssistantFailed(AssistantProblem.notAllowed), ); expect(cubit.state.agreeing, isNull); }); test("an offer that can't be read offers nothing", () async { gateway.failing = const AssistantFailed(AssistantProblem.failed); await cubit.load(); expect( cubit.state.unreadable, const AssistantFailed(AssistantProblem.failed), ); expect(cubit.state.offer, AssistantsOffer.none); }); test('an app keeping no own assistants keeps none', () async { final without = AssistantsCubit(repository); addTearDown(without.close); expect(await without.saveOwn(_own), OwnAssistantSaved.failed); }); }); group('AssistantGrantsCubit', () { late FakeAssistantOperatorGateway gateway; late FakeInstanceOperatorGateway accounts; late AssistantOperatorRepository repository; late AssistantGrantsCubit cubit; setUp(() { gateway = FakeAssistantOperatorGateway() ..set = _setUp ..accounts.addAll({'anna': 'anna'}); accounts = FakeInstanceOperatorGateway() ..operates = true ..add( OperatedAccount(id: 'anna', handle: 'anna', since: DateTime(2026)), ); repository = AssistantOperatorRepository(gateway, accounts); cubit = AssistantGrantsCubit(repository); addTearDown(() async { await cubit.close(); await repository.dispose(); }); }); test('reads the assistants set and the grants', () async { await cubit.load(); expect(cubit.state.setUp, _setUp); expect(cubit.state.grants, isEmpty); expect(cubit.state.showsAnything, isTrue); }); test('shows nothing without an assistant set or a grant', () async { gateway.set = const AssistantsSetUp(); await cubit.load(); expect(cubit.state.showsAnything, isFalse); }); test('granting by handle reads the grants anew', () async { await cubit.load(); final failed = await cubit.grant( handle: 'anna', assistant: 'claude', dailyLimit: 1000000, monthlyLimit: null, ); expect(failed, isNull); expect(cubit.state.grants.single.account, 'anna'); expect(cubit.state.busy, isNull); }); test("a handle nobody has is the sheet's to say, not the " "screen's", () async { await cubit.load(); final failed = await cubit.grant( handle: 'bob', assistant: 'claude', dailyLimit: null, monthlyLimit: null, ); expect(failed, const OperatorFailed(OperatorProblem.notFound)); expect(cubit.state.problem, isNull); expect(cubit.state.grants, isEmpty); }); test('changes the limits, and takes a grant back', () async { await cubit.load(); await cubit.grant( handle: 'anna', assistant: 'claude', dailyLimit: 1, monthlyLimit: 2, ); expect( await cubit.changeLimits( cubit.state.grants.single, dailyLimit: 5, monthlyLimit: null, ), isNull, ); expect(cubit.state.grants.single.dailyLimit, 5); expect(cubit.state.grants.single.monthlyLimit, isNull); await cubit.revoke(cubit.state.grants.single); expect(cubit.state.grants, isEmpty); }); test("taking back what isn't granted any more is the screen's " 'problem', () async { await cubit.load(); await cubit.revoke( AssistantGrant( account: 'anna', assistant: 'claude', grantedAt: DateTime.utc(2026, 10, 25), ), ); expect( cubit.state.problem, const OperatorFailed(OperatorProblem.notFound), ); }); test("someone who doesn't operate the instance reads nothing", () async { gateway.failing = const OperatorFailed(OperatorProblem.notAllowed); await cubit.load(); expect( cubit.state.unreadable, const OperatorFailed(OperatorProblem.notAllowed), ); expect(cubit.state.showsAnything, isFalse); }); }); group('AssistantUseCubit', () { test('offers this UTC month and the two before, and reads the one ' 'shown', () async { final asked = []; final use = AssistantUse( assistant: 'claude', month: DateTime.utc(2026), requests: 2, ); final cubit = withClock( Clock.fixed(DateTime.utc(2026, 1, 31, 23, 30)), () => AssistantUseCubit((month) async { asked.add(month); return [use]; }), ); addTearDown(cubit.close); expect(cubit.state.months, [ DateTime.utc(2026), DateTime.utc(2025, 12), DateTime.utc(2025, 11), ]); await cubit.show(); expect(cubit.state.use, [use]); expect(cubit.state.loading, isFalse); await cubit.show(DateTime.utc(2025, 11)); expect(cubit.state.month, DateTime.utc(2025, 11)); expect(asked, [DateTime.utc(2026), DateTime.utc(2025, 11)]); }); test("says when the use couldn't be read", () async { final cubit = AssistantUseCubit( (month) async => throw const AssistantFailed(AssistantProblem.failed), ); addTearDown(cubit.close); await cubit.show(); expect(cubit.state.failed, isTrue); expect(cubit.state.use, isEmpty); }); }); }