feat(base)!: one budget per person for all their assistants, the effort each takes, the operator's lists by page
A grant now says only which assistant a person may use, and keeps their agreement; the limits are the person's (assistant_budget): one per UTC day and month in money and in tokens, each optional, money counting the assistants with prices, tokens every one (Halkyon Learn's ADR 0021, D58, D59). assistantOperator.grant gives several people several assistants with one budget, change sets one person's exactly, revoke takes back everything; grantees and granteesOf list people with their assistants, budget and use, 50 at a time. The migration carries each person's grant limits into a budget: per unit, their sum, a grant without a limit making it none. assistants.env gives each assistant a default and a highest effort (_EFFORT, _MAX_EFFORT; refused at start where the provider takes none); ask takes one within that range, refusing another (invalidEffort), and caps the output at its ceiling (D63). instanceOperator.waiting and accounts come 50 at a time from an offset, and counts gives the rows of the operator's screen (D62). BREAKING CHANGE: the offer, the grants and the operator's lists changed shape; Learn's server now asks for build 25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0590a4b43d
commit
24ed63585e
5 changed files with 50672 additions and 1371 deletions
|
|
@ -736,13 +736,18 @@ void main() {
|
|||
final carla = await accountOf('Carla_M');
|
||||
final calls =
|
||||
<String, Future<Object?> Function(TestSessionBuilder)>{
|
||||
'waiting': (as) => endpoints.instanceOperator.waiting(as),
|
||||
'counts': (as) => endpoints.instanceOperator.counts(as),
|
||||
'waiting': (as) =>
|
||||
endpoints.instanceOperator.waiting(as, offset: 0),
|
||||
'approve': (as) =>
|
||||
endpoints.instanceOperator.approve(as, account: carla),
|
||||
'refuse': (as) =>
|
||||
endpoints.instanceOperator.refuse(as, account: carla),
|
||||
'accounts': (as) =>
|
||||
endpoints.instanceOperator.accounts(as, startingWith: ''),
|
||||
'accounts': (as) => endpoints.instanceOperator.accounts(
|
||||
as,
|
||||
startingWith: '',
|
||||
offset: 0,
|
||||
),
|
||||
'suspend': (as) =>
|
||||
endpoints.instanceOperator.suspend(as, account: olga),
|
||||
'operators': (as) => endpoints.instanceOperator.operators(as),
|
||||
|
|
@ -778,13 +783,22 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test('list who waits, oldest first, with handle and words, suspended '
|
||||
'ones marked', () async {
|
||||
test('list who waits, oldest first, a page at a time, with handle '
|
||||
'and words, suspended ones marked (D62)', () async {
|
||||
useSignUpMode(pod, SignUpMode.approval);
|
||||
final carlas = await signUpToWait();
|
||||
await endpoints.instanceOperator.suspend(asOlga(), account: ben);
|
||||
|
||||
final waiting = await endpoints.instanceOperator.waiting(asOlga());
|
||||
final waiting = await endpoints.instanceOperator.waiting(
|
||||
asOlga(),
|
||||
offset: 0,
|
||||
);
|
||||
final after = await endpoints.instanceOperator.waiting(
|
||||
asOlga(),
|
||||
offset: 1,
|
||||
);
|
||||
|
||||
expect([for (final account in after) account.handle], ['ben']);
|
||||
|
||||
expect(
|
||||
[
|
||||
|
|
@ -825,8 +839,20 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test('count who waits, who operates and every account (D62)', () async {
|
||||
useSignUpMode(pod, SignUpMode.approval);
|
||||
await signUpToWait();
|
||||
|
||||
final counts = await endpoints.instanceOperator.counts(asOlga());
|
||||
|
||||
expect(
|
||||
(counts.waiting, counts.operators, counts.accounts),
|
||||
(1, 1, await AuthUser.db.count(session)),
|
||||
);
|
||||
});
|
||||
|
||||
test('find accounts by the start of their handle, whatever its case, '
|
||||
'by handle, at most 50', () async {
|
||||
'by handle, 50 at a time (D62)', () async {
|
||||
for (var i = 0; i < 52; i++) {
|
||||
await newPersonWithAddress(
|
||||
session,
|
||||
|
|
@ -838,14 +864,28 @@ void main() {
|
|||
final found = await endpoints.instanceOperator.accounts(
|
||||
asOlga(),
|
||||
startingWith: 'BEE',
|
||||
offset: 0,
|
||||
);
|
||||
final more = await endpoints.instanceOperator.accounts(
|
||||
asOlga(),
|
||||
startingWith: 'BEE',
|
||||
offset: 50,
|
||||
);
|
||||
|
||||
expect(found, hasLength(50));
|
||||
expect(found.first.handle, 'Bee00');
|
||||
expect(found.last.handle, 'Bee49');
|
||||
expect(
|
||||
[for (final account in more) account.handle],
|
||||
[
|
||||
'Bee50',
|
||||
'Bee51',
|
||||
],
|
||||
);
|
||||
final olgas = (await endpoints.instanceOperator.accounts(
|
||||
asOlga(),
|
||||
startingWith: 'olg',
|
||||
offset: 0,
|
||||
)).single;
|
||||
expect(
|
||||
(olgas.account, olgas.isOperator, olgas.waitingSince),
|
||||
|
|
@ -859,6 +899,7 @@ void main() {
|
|||
await endpoints.instanceOperator.accounts(
|
||||
asOlga(),
|
||||
startingWith: '%',
|
||||
offset: 0,
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ import '../test_tools/serverpod_test_tools.dart';
|
|||
|
||||
// The assistants an operator grants (Halkyon Learn's ADR 0021, step 2): what
|
||||
// a person is offered and agrees to (D40), a question forwarded to the
|
||||
// provider and its answer streamed back (D35), refused without a grant, an
|
||||
// agreement, room within the limits or a question the provider can take;
|
||||
// what each request counts (D38, D42), kept 3 months (D41) and deleted with
|
||||
// the person; and the operator's grants and figures, refused to anyone else.
|
||||
// provider and its answer streamed back (D35) at the effort chosen (D63),
|
||||
// refused without a grant, an agreement, room within the person's budget
|
||||
// (D58, D59) or a question the provider can take; what each request counts
|
||||
// (D38, D42), kept 3 months (D41) and deleted with the person; and the
|
||||
// operator's grants, budgets and figures, refused to anyone else.
|
||||
// The provider is a FakeLlmClient: halkyon_llm's own tests hold the
|
||||
// protocols.
|
||||
|
||||
/// 22:30 UTC on 25 Oct 2026: the day's limit resets at midnight, the
|
||||
/// 22:30 UTC on 25 Oct 2026: the day's budget resets at midnight, the
|
||||
/// month's on 1 Nov.
|
||||
final _now = DateTime.utc(2026, 10, 25, 22, 30);
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ AssistantQuestion _question({
|
|||
List<AssistantMessage>? messages,
|
||||
List<AssistantTool> tools = const [],
|
||||
int maxOutputTokens = 2048,
|
||||
AssistantEffort? effort,
|
||||
}) => AssistantQuestion(
|
||||
assistant: assistant,
|
||||
system: 'Du hilfst beim Japanischlernen.',
|
||||
|
|
@ -70,8 +72,34 @@ AssistantQuestion _question({
|
|||
],
|
||||
tools: tools,
|
||||
maxOutputTokens: maxOutputTokens,
|
||||
effort: effort,
|
||||
);
|
||||
|
||||
/// A budget of [dailyCost] and [monthlyCost] in millionths, and
|
||||
/// [dailyTokens] and [monthlyTokens]; none of them: no limits.
|
||||
AssistantLimits _budget({
|
||||
int? dailyCost,
|
||||
int? monthlyCost,
|
||||
int? dailyTokens,
|
||||
int? monthlyTokens,
|
||||
}) => AssistantLimits(
|
||||
dailyCostMicros: dailyCost,
|
||||
monthlyCostMicros: monthlyCost,
|
||||
dailyTokens: dailyTokens,
|
||||
monthlyTokens: monthlyTokens,
|
||||
);
|
||||
|
||||
/// The limits of [budget], to compare.
|
||||
(int?, int?, int?, int?) _limitsOf(AssistantLimits budget) => (
|
||||
budget.dailyCostMicros,
|
||||
budget.monthlyCostMicros,
|
||||
budget.dailyTokens,
|
||||
budget.monthlyTokens,
|
||||
);
|
||||
|
||||
/// What [use] says, to compare.
|
||||
(int, int) _useOf(AssistantUse use) => (use.costMicros, use.tokens);
|
||||
|
||||
Matcher _refused(AssistantRefusal refusal) => throwsA(
|
||||
isA<AssistantRefusedException>().having(
|
||||
(e) => e.refusal,
|
||||
|
|
@ -156,26 +184,23 @@ void main() {
|
|||
Future<T> atNow<T>(Future<T> Function() body) =>
|
||||
withClock(Clock.fixed(_now), body);
|
||||
|
||||
/// Grants Ben [assistant] with [daily] and [monthly] limits, as Olga.
|
||||
/// Grants Ben [assistant] with [budget] (none: no limits), as Olga.
|
||||
Future<void> grantBen({
|
||||
String assistant = 'claude',
|
||||
int? daily,
|
||||
int? monthly,
|
||||
AssistantLimits? budget,
|
||||
}) => endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
account: ben,
|
||||
assistant: assistant,
|
||||
dailyLimit: daily,
|
||||
monthlyLimit: monthly,
|
||||
accounts: [ben],
|
||||
assistants: [assistant],
|
||||
budget: budget ?? _budget(),
|
||||
);
|
||||
|
||||
/// Grants Ben [assistant], and he agrees to it.
|
||||
Future<void> grantAndAgree({
|
||||
String assistant = 'claude',
|
||||
int? daily,
|
||||
int? monthly,
|
||||
AssistantLimits? budget,
|
||||
}) async {
|
||||
await grantBen(assistant: assistant, daily: daily, monthly: monthly);
|
||||
await grantBen(assistant: assistant, budget: budget);
|
||||
await endpoints.assistant.agree(asBen(), assistant: assistant);
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +213,6 @@ void main() {
|
|||
authUserId: ben,
|
||||
assistant: assistant,
|
||||
grantedAt: _now,
|
||||
limitUnit: AssistantLimitUnit.tokens,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -391,21 +415,91 @@ void main() {
|
|||
expect(jsonDecode(called.extra!), {'x': 1});
|
||||
});
|
||||
|
||||
test("holds the ceiling on output to the instance's", () async {
|
||||
test("holds the ceiling on output to the effort's, the model's own "
|
||||
"default's without one (D63)", () async {
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'xhigh',
|
||||
}),
|
||||
);
|
||||
await grantAndAgree();
|
||||
await grantAndAgree(assistant: 'local');
|
||||
provider
|
||||
..replyText('a')
|
||||
..replyText('b');
|
||||
..replyText('b')
|
||||
..replyText('c')
|
||||
..replyText('d');
|
||||
|
||||
await askAsBen(_question(maxOutputTokens: 100000));
|
||||
await askAsBen(
|
||||
_question(maxOutputTokens: 100000, effort: AssistantEffort.xhigh),
|
||||
);
|
||||
await askAsBen(_question(maxOutputTokens: 0));
|
||||
await askAsBen(
|
||||
_question(assistant: 'local', maxOutputTokens: 100000),
|
||||
);
|
||||
|
||||
expect(
|
||||
[for (final (_, request) in provider.sent) request.maxOutputTokens],
|
||||
[assistantOutputCeiling, 1],
|
||||
[
|
||||
LlmEffort.medium.outputCeiling,
|
||||
LlmEffort.xhigh.outputCeiling,
|
||||
1,
|
||||
LlmEffort.defaultOutputCeiling,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("asks at the effort the person picks within the operator's "
|
||||
"range, else at the operator's default, else at the model's own "
|
||||
'(D63)', () async {
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'high',
|
||||
}),
|
||||
);
|
||||
await grantAndAgree();
|
||||
await grantAndAgree(assistant: 'local');
|
||||
provider
|
||||
..replyText('a')
|
||||
..replyText('b')
|
||||
..replyText('c')
|
||||
..replyText('d');
|
||||
|
||||
await askAsBen(_question());
|
||||
await askAsBen(_question(effort: AssistantEffort.low));
|
||||
await askAsBen(_question(effort: AssistantEffort.high));
|
||||
await askAsBen(_question(assistant: 'local'));
|
||||
|
||||
expect(
|
||||
[for (final (_, request) in provider.sent) request.effort],
|
||||
[LlmEffort.medium, LlmEffort.low, LlmEffort.high, null],
|
||||
);
|
||||
});
|
||||
|
||||
test('money stops only the assistants with prices; tokens stop every '
|
||||
'one (D59)', () async {
|
||||
// Each grant sets the person's budget, so the second sets it again.
|
||||
await grantAndAgree(budget: _budget(dailyCost: 100));
|
||||
await grantAndAgree(
|
||||
assistant: 'local',
|
||||
budget: _budget(dailyCost: 100),
|
||||
);
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 100);
|
||||
provider.replyText('犬');
|
||||
|
||||
await expectLater(
|
||||
atNow(() => askAsBen(_question())),
|
||||
_refused(AssistantRefusal.limitReached),
|
||||
);
|
||||
await atNow(() => askAsBen(_question(assistant: 'local')));
|
||||
expect(provider.sent.single.$1.model, 'qwen3:8b');
|
||||
});
|
||||
|
||||
test('an answer cancelled is counted to its end all the same, so '
|
||||
"cancelling can't slip past a limit", () async {
|
||||
await grantAndAgree();
|
||||
|
|
@ -543,10 +637,16 @@ void main() {
|
|||
});
|
||||
|
||||
test(
|
||||
"past a day's limit, saying it resets at UTC's midnight (D42)",
|
||||
"past a day's budget in money, all of the person's assistants "
|
||||
"counted together, saying it resets at UTC's midnight (D42, D58)",
|
||||
() async {
|
||||
await grantAndAgree(daily: 10000);
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 10000);
|
||||
await grantAndAgree(budget: _budget(dailyCost: 10000));
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 6000);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
assistant: 'haiku',
|
||||
costMicros: 4000,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
atNow(() => askAsBen(_question())),
|
||||
|
|
@ -567,9 +667,11 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test("past a month's limit, saying it resets on the next month's "
|
||||
"first, which a day's limit doesn't change", () async {
|
||||
await grantAndAgree(daily: 10000, monthly: 25000);
|
||||
test("past a month's budget, saying it resets on the next month's "
|
||||
"first, which a day's budget doesn't change", () async {
|
||||
await grantAndAgree(
|
||||
budget: _budget(dailyCost: 10000, monthlyCost: 25000),
|
||||
);
|
||||
await usedBefore(DateTime.utc(2026, 10, 3), costMicros: 15000);
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 10000);
|
||||
|
||||
|
|
@ -591,12 +693,22 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('in tokens, where the assistant has no prices', () async {
|
||||
await grantAndAgree(assistant: 'local', daily: 1000);
|
||||
test('past a budget in tokens, every assistant counted, priced or '
|
||||
'not (D59)', () async {
|
||||
await grantAndAgree(budget: _budget(dailyTokens: 1000));
|
||||
await grantAndAgree(
|
||||
assistant: 'local',
|
||||
budget: _budget(dailyTokens: 1000),
|
||||
);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
assistant: 'local',
|
||||
inputTokens: 900,
|
||||
inputTokens: 500,
|
||||
outputTokens: 100,
|
||||
);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
inputTokens: 300,
|
||||
outputTokens: 100,
|
||||
);
|
||||
|
||||
|
|
@ -604,22 +716,33 @@ void main() {
|
|||
atNow(() => askAsBen(_question(assistant: 'local'))),
|
||||
_refused(AssistantRefusal.limitReached),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(() => askAsBen(_question())),
|
||||
_refused(AssistantRefusal.limitReached),
|
||||
);
|
||||
});
|
||||
|
||||
test('once its prices came or went, until the operator sets the '
|
||||
'limits again', () async {
|
||||
await grantAndAgree(daily: 10000);
|
||||
test("with an effort above the operator's highest, or to an "
|
||||
'assistant without a range (D63)', () async {
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_PRICE_INPUT': '',
|
||||
'ASSISTANT_CLAUDE_PRICE_OUTPUT': '',
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'high',
|
||||
}),
|
||||
);
|
||||
await grantAndAgree();
|
||||
await grantAndAgree(assistant: 'local');
|
||||
|
||||
await expectLater(
|
||||
askAsBen(_question()),
|
||||
_refused(AssistantRefusal.limitOutdated),
|
||||
askAsBen(_question(effort: AssistantEffort.xhigh)),
|
||||
_refused(AssistantRefusal.invalidEffort),
|
||||
);
|
||||
await expectLater(
|
||||
askAsBen(
|
||||
_question(assistant: 'local', effort: AssistantEffort.low),
|
||||
),
|
||||
_refused(AssistantRefusal.invalidEffort),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -835,12 +958,43 @@ void main() {
|
|||
|
||||
group('what a person is offered (point 6, D27)', () {
|
||||
test("the assistants granted that are set, in the operator's order, "
|
||||
'with what goes where, the limits and what is left', () async {
|
||||
await grantBen(assistant: 'local', daily: 5000);
|
||||
await grantAndAgree(daily: 10000, monthly: 50000);
|
||||
'with what goes where and their effort, and the budget with what '
|
||||
'is used of it, every assistant together', () async {
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'high',
|
||||
}),
|
||||
);
|
||||
await grantBen(assistant: 'local');
|
||||
await grantAndAgree(
|
||||
budget: _budget(
|
||||
dailyCost: 10000,
|
||||
monthlyCost: 50000,
|
||||
monthlyTokens: 100000,
|
||||
),
|
||||
);
|
||||
await grantedBefore('gone');
|
||||
await usedBefore(DateTime.utc(2026, 10, 3), costMicros: 7000);
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 2500);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 3),
|
||||
costMicros: 7000,
|
||||
inputTokens: 600,
|
||||
outputTokens: 100,
|
||||
);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
costMicros: 2500,
|
||||
inputTokens: 100,
|
||||
outputTokens: 20,
|
||||
);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
assistant: 'local',
|
||||
inputTokens: 250,
|
||||
outputTokens: 50,
|
||||
);
|
||||
await usedBefore(DateTime.utc(2026, 9, 30), costMicros: 1);
|
||||
|
||||
final offered = await atNow(
|
||||
() => endpoints.assistant.offered(asBen()),
|
||||
|
|
@ -856,26 +1010,43 @@ void main() {
|
|||
expect(claude.model, 'claude-sonnet-5-5');
|
||||
expect(claude.images, isTrue);
|
||||
expect(claude.agreedAt, isNotNull);
|
||||
expect(claude.limitUnit, AssistantLimitUnit.costMicros);
|
||||
expect((claude.dailyLimit, claude.monthlyLimit), (10000, 50000));
|
||||
expect((claude.usedToday, claude.usedThisMonth), (2500, 9500));
|
||||
expect(claude.outdated, isFalse);
|
||||
expect(claude.effort, AssistantEffort.medium);
|
||||
expect(claude.efforts, [
|
||||
AssistantEffort.low,
|
||||
AssistantEffort.medium,
|
||||
AssistantEffort.high,
|
||||
]);
|
||||
expect(local.id, 'local');
|
||||
expect(local.host, 'ollama');
|
||||
expect(local.agreedAt, isNull);
|
||||
expect(local.limitUnit, AssistantLimitUnit.tokens);
|
||||
expect((local.usedToday, local.usedThisMonth), (0, 0));
|
||||
expect(local.effort, isNull);
|
||||
expect(local.efforts, isEmpty);
|
||||
expect(_limitsOf(offered.budget), (10000, 50000, null, 100000));
|
||||
expect(_useOf(offered.usedToday), (2500, 420));
|
||||
expect(_useOf(offered.usedThisMonth), (9500, 1120));
|
||||
});
|
||||
|
||||
test('an agreement to another provider counts as none; the grant '
|
||||
'whose prices came or went says so', () async {
|
||||
await grantAndAgree(daily: 10000);
|
||||
test(
|
||||
'no budget: no limits, what is used counted all the same',
|
||||
() async {
|
||||
await grantAndAgree();
|
||||
await usedBefore(DateTime.utc(2026, 10, 25), costMicros: 30);
|
||||
|
||||
final offered = await atNow(
|
||||
() => endpoints.assistant.offered(asBen()),
|
||||
);
|
||||
|
||||
expect(_limitsOf(offered.budget), (null, null, null, null));
|
||||
expect(_useOf(offered.usedToday), (30, 0));
|
||||
},
|
||||
);
|
||||
|
||||
test('an agreement to another provider counts as none', () async {
|
||||
await grantAndAgree();
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_BASE_URL': 'https://claude.proxy.example.org',
|
||||
'ASSISTANT_CLAUDE_PRICE_INPUT': '',
|
||||
'ASSISTANT_CLAUDE_PRICE_OUTPUT': '',
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -884,7 +1055,6 @@ void main() {
|
|||
.single;
|
||||
expect(claude.host, 'claude.proxy.example.org');
|
||||
expect(claude.agreedAt, isNull);
|
||||
expect(claude.outdated, isTrue);
|
||||
});
|
||||
|
||||
test('a new model of the same provider keeps the agreement', () async {
|
||||
|
|
@ -996,7 +1166,7 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group("the operator's calls (points 6 and 8)", () {
|
||||
group("the operator's calls (points 6 and 8, D58)", () {
|
||||
test(
|
||||
'are refused to anyone but an operator, signed in or not',
|
||||
() async {
|
||||
|
|
@ -1004,19 +1174,29 @@ void main() {
|
|||
final calls =
|
||||
<String, Future<Object?> Function(TestSessionBuilder)>{
|
||||
'setUp': (as) => endpoints.assistantOperator.setUp(as),
|
||||
'grants': (as) => endpoints.assistantOperator.grants(as),
|
||||
'grantees': (as) => endpoints.assistantOperator.grantees(
|
||||
as,
|
||||
startingWith: '',
|
||||
offset: 0,
|
||||
),
|
||||
'granteesOf': (as) => endpoints.assistantOperator.granteesOf(
|
||||
as,
|
||||
accounts: [ben],
|
||||
),
|
||||
'grant': (as) => endpoints.assistantOperator.grant(
|
||||
as,
|
||||
account: ben,
|
||||
assistant: 'local',
|
||||
dailyLimit: null,
|
||||
monthlyLimit: null,
|
||||
accounts: [ben],
|
||||
assistants: ['local'],
|
||||
budget: _budget(),
|
||||
),
|
||||
'revoke': (as) => endpoints.assistantOperator.revoke(
|
||||
'change': (as) => endpoints.assistantOperator.change(
|
||||
as,
|
||||
account: ben,
|
||||
assistant: 'claude',
|
||||
assistants: ['local'],
|
||||
budget: _budget(),
|
||||
),
|
||||
'revoke': (as) =>
|
||||
endpoints.assistantOperator.revoke(as, account: ben),
|
||||
'figures': (as) => endpoints.assistantOperator.figures(
|
||||
as,
|
||||
month: DateTime.utc(2026, 10),
|
||||
|
|
@ -1041,11 +1221,23 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test('see the assistants set, without their keys', () async {
|
||||
test('see the assistants set, without their keys, with their effort '
|
||||
'and how many people have any', () async {
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'high',
|
||||
}),
|
||||
);
|
||||
await grantBen();
|
||||
await grantBen(assistant: 'local');
|
||||
|
||||
final setUp = await endpoints.assistantOperator.setUp(asOlga());
|
||||
|
||||
expect(setUp.currency, 'EUR');
|
||||
expect(setUp.ownKeys, isFalse);
|
||||
expect(setUp.people, 1);
|
||||
final [claude, local] = setUp.assistants;
|
||||
expect(
|
||||
(
|
||||
|
|
@ -1065,96 +1257,228 @@ void main() {
|
|||
);
|
||||
expect((claude.priceInput, claude.priceOutput), (3.0, 15.0));
|
||||
expect(claude.images, isTrue);
|
||||
expect(
|
||||
(claude.effort, claude.maxEffort),
|
||||
(AssistantEffort.medium, AssistantEffort.high),
|
||||
);
|
||||
expect((local.protocol, local.host), ('openai', 'ollama'));
|
||||
expect(local.priceInput, isNull);
|
||||
expect((local.effort, local.maxEffort), (null, null));
|
||||
expect(jsonEncode(setUp.toJson()), isNot(contains('sk-ant-test')));
|
||||
});
|
||||
|
||||
test("grant in the unit of the assistant's pricing; granted again, "
|
||||
'the limits change and the agreement stays', () async {
|
||||
await grantAndAgree(daily: 10000);
|
||||
await grantBen(assistant: 'local', monthly: 200000);
|
||||
await grantBen(daily: 20000, monthly: 90000);
|
||||
|
||||
final grants = await endpoints.assistantOperator.grants(asOlga());
|
||||
|
||||
final [claude, local] = grants;
|
||||
expect((claude.account, claude.handle), (ben, 'ben'));
|
||||
expect(claude.assistant, 'claude');
|
||||
expect(claude.limitUnit, AssistantLimitUnit.costMicros);
|
||||
expect((claude.dailyLimit, claude.monthlyLimit), (20000, 90000));
|
||||
expect(claude.agreedAt, isNotNull);
|
||||
expect(claude.assistantSet, isTrue);
|
||||
expect(claude.outdated, isFalse);
|
||||
expect(local.limitUnit, AssistantLimitUnit.tokens);
|
||||
expect((local.dailyLimit, local.monthlyLimit), (null, 200000));
|
||||
expect(local.agreedAt, isNull);
|
||||
});
|
||||
|
||||
test('a grant of an assistant set no more is shown so, and an '
|
||||
'outdated one too (D44)', () async {
|
||||
await grantBen(daily: 1);
|
||||
await grantBen(assistant: 'local');
|
||||
useAssistants(
|
||||
pod,
|
||||
_assistants({
|
||||
'ASSISTANTS': 'claude',
|
||||
'ASSISTANT_CLAUDE_PRICE_INPUT': '',
|
||||
'ASSISTANT_CLAUDE_PRICE_OUTPUT': '',
|
||||
}),
|
||||
);
|
||||
|
||||
final grants = await endpoints.assistantOperator.grants(asOlga());
|
||||
|
||||
final [claude, local] = grants;
|
||||
expect((claude.assistantSet, claude.outdated), (true, true));
|
||||
expect(local.assistantSet, isFalse);
|
||||
});
|
||||
|
||||
test('granting is refused for an assistant not set, a limit below 0, '
|
||||
'and an account unknown', () async {
|
||||
await expectLater(
|
||||
grantBen(assistant: 'gpt'),
|
||||
_refusedForOperator(OperatorRefusal.assistantNotSet),
|
||||
);
|
||||
await expectLater(
|
||||
grantBen(daily: -1),
|
||||
_refusedForOperator(OperatorRefusal.invalidLimit),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
account: UuidValue.fromString(
|
||||
'0193a3d4-5c6e-7f80-9a1b-2c3d4e5f6a7b',
|
||||
),
|
||||
assistant: 'claude',
|
||||
dailyLimit: null,
|
||||
monthlyLimit: null,
|
||||
),
|
||||
_refusedForOperator(OperatorRefusal.notFound),
|
||||
);
|
||||
expect(await AssistantGrant.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('revoke a grant, which then refuses; a grant there is not is '
|
||||
'refused', () async {
|
||||
await grantAndAgree();
|
||||
await endpoints.assistantOperator.revoke(
|
||||
test('grant several people several assistants at once, each with the '
|
||||
'budget set; granted before, the assistants add up, the '
|
||||
'agreements stay and the budget is the new one', () async {
|
||||
await endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
account: ben,
|
||||
assistant: 'claude',
|
||||
accounts: [ben, olga],
|
||||
assistants: ['claude'],
|
||||
budget: _budget(dailyCost: 10000),
|
||||
);
|
||||
await endpoints.assistant.agree(asBen(), assistant: 'claude');
|
||||
await endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
accounts: [ben],
|
||||
assistants: ['local', 'claude'],
|
||||
budget: _budget(monthlyCost: 90000, monthlyTokens: 200000),
|
||||
);
|
||||
|
||||
final grantees = await endpoints.assistantOperator.grantees(
|
||||
asOlga(),
|
||||
startingWith: '',
|
||||
offset: 0,
|
||||
);
|
||||
|
||||
final [bens, olgas] = grantees;
|
||||
expect((bens.account, bens.handle), (ben, 'ben'));
|
||||
expect(
|
||||
[for (final each in bens.assistants) each.assistant],
|
||||
[
|
||||
'claude',
|
||||
'local',
|
||||
],
|
||||
);
|
||||
final [claude, local] = bens.assistants;
|
||||
expect(claude.assistantSet, isTrue);
|
||||
expect(claude.agreedAt, isNotNull);
|
||||
expect(local.agreedAt, isNull);
|
||||
expect(_limitsOf(bens.budget), (null, 90000, null, 200000));
|
||||
expect((olgas.account, olgas.handle), (olga, 'Olga'));
|
||||
expect(
|
||||
[for (final each in olgas.assistants) each.assistant],
|
||||
[
|
||||
'claude',
|
||||
],
|
||||
);
|
||||
expect(_limitsOf(olgas.budget), (10000, null, null, null));
|
||||
});
|
||||
|
||||
test(
|
||||
"change a person's assistants and budget to exactly those given: "
|
||||
'the assistants left out go, the agreements of those kept stay',
|
||||
() async {
|
||||
await grantAndAgree(budget: _budget(dailyCost: 10000));
|
||||
await grantBen(assistant: 'local');
|
||||
await endpoints.assistantOperator.change(
|
||||
asOlga(),
|
||||
account: ben,
|
||||
assistants: ['claude'],
|
||||
budget: _budget(dailyTokens: 5000),
|
||||
);
|
||||
|
||||
final [bens] = await endpoints.assistantOperator.granteesOf(
|
||||
asOlga(),
|
||||
accounts: [ben, olga],
|
||||
);
|
||||
|
||||
final claude = bens.assistants.single;
|
||||
expect(claude.assistant, 'claude');
|
||||
expect(claude.agreedAt, isNotNull);
|
||||
expect(_limitsOf(bens.budget), (null, null, 5000, null));
|
||||
},
|
||||
);
|
||||
|
||||
test('grantees are found by the start of their handle, whatever its '
|
||||
'case, by handle, a page at a time, with what they used, those '
|
||||
'without a grant left out', () async {
|
||||
final cleo = await newPersonWithAddress(
|
||||
session,
|
||||
'cleo@example.org',
|
||||
handle: 'Cleo',
|
||||
);
|
||||
await newPersonWithAddress(
|
||||
session,
|
||||
'bert@example.org',
|
||||
handle: 'bert',
|
||||
);
|
||||
await endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
accounts: [ben, cleo, olga],
|
||||
assistants: ['claude'],
|
||||
budget: _budget(),
|
||||
);
|
||||
await usedBefore(
|
||||
DateTime.utc(2026, 10, 25),
|
||||
costMicros: 300,
|
||||
inputTokens: 30,
|
||||
outputTokens: 5,
|
||||
);
|
||||
|
||||
Future<List<String?>> handles(String start, int offset) async => [
|
||||
for (final each in await atNow(
|
||||
() => endpoints.assistantOperator.grantees(
|
||||
asOlga(),
|
||||
startingWith: start,
|
||||
offset: offset,
|
||||
),
|
||||
))
|
||||
each.handle,
|
||||
];
|
||||
|
||||
expect(await handles('', 0), ['ben', 'Cleo', 'Olga']);
|
||||
expect(await handles('', 2), ['Olga']);
|
||||
expect(await handles('B', 0), ['ben']);
|
||||
expect(await handles('%', 0), isEmpty);
|
||||
final bens = (await atNow(
|
||||
() => endpoints.assistantOperator.grantees(
|
||||
asOlga(),
|
||||
startingWith: 'ben',
|
||||
offset: 0,
|
||||
),
|
||||
)).single;
|
||||
expect(_useOf(bens.usedToday), (300, 35));
|
||||
expect(_useOf(bens.usedThisMonth), (300, 35));
|
||||
});
|
||||
|
||||
test('a grant of an assistant set no more is shown so (D44)', () async {
|
||||
await grantBen();
|
||||
await grantBen(assistant: 'local');
|
||||
useAssistants(pod, _assistants({'ASSISTANTS': 'claude'}));
|
||||
|
||||
final [bens] = await endpoints.assistantOperator.granteesOf(
|
||||
asOlga(),
|
||||
accounts: [ben],
|
||||
);
|
||||
|
||||
final [claude, local] = bens.assistants;
|
||||
expect(claude.assistantSet, isTrue);
|
||||
expect((local.assistant, local.assistantSet), ('local', false));
|
||||
});
|
||||
|
||||
test(
|
||||
'granting and changing are refused for an assistant not set, a '
|
||||
'limit below 0, and an account unknown, changing nothing',
|
||||
() async {
|
||||
final nobody = UuidValue.fromString(
|
||||
'0193a3d4-5c6e-7f80-9a1b-2c3d4e5f6a7b',
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
accounts: [ben],
|
||||
assistants: ['claude', 'gpt'],
|
||||
budget: _budget(),
|
||||
),
|
||||
_refusedForOperator(OperatorRefusal.assistantNotSet),
|
||||
);
|
||||
for (final budget in [
|
||||
_budget(dailyCost: -1),
|
||||
_budget(monthlyCost: -1),
|
||||
_budget(dailyTokens: -1),
|
||||
_budget(monthlyTokens: -1),
|
||||
]) {
|
||||
await expectLater(
|
||||
grantBen(budget: budget),
|
||||
_refusedForOperator(OperatorRefusal.invalidLimit),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.grant(
|
||||
asOlga(),
|
||||
accounts: [ben, nobody],
|
||||
assistants: ['claude'],
|
||||
budget: _budget(),
|
||||
),
|
||||
_refusedForOperator(OperatorRefusal.notFound),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.change(
|
||||
asOlga(),
|
||||
account: nobody,
|
||||
assistants: ['claude'],
|
||||
budget: _budget(),
|
||||
),
|
||||
_refusedForOperator(OperatorRefusal.notFound),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.change(
|
||||
asOlga(),
|
||||
account: ben,
|
||||
assistants: ['gpt'],
|
||||
budget: _budget(),
|
||||
),
|
||||
_refusedForOperator(OperatorRefusal.assistantNotSet),
|
||||
);
|
||||
expect(await AssistantGrant.db.count(session), 0);
|
||||
expect(await AssistantBudget.db.count(session), 0);
|
||||
},
|
||||
);
|
||||
|
||||
test("revoke a person's assistants and budget, which then refuses; "
|
||||
'a person without any is refused', () async {
|
||||
await grantAndAgree(budget: _budget(dailyCost: 1000));
|
||||
await grantBen(assistant: 'local');
|
||||
await endpoints.assistantOperator.revoke(asOlga(), account: ben);
|
||||
|
||||
await expectLater(
|
||||
askAsBen(_question()),
|
||||
_refused(AssistantRefusal.notGranted),
|
||||
);
|
||||
expect(await AssistantGrant.db.count(session), 0);
|
||||
expect(await AssistantBudget.db.count(session), 0);
|
||||
await expectLater(
|
||||
endpoints.assistantOperator.revoke(
|
||||
asOlga(),
|
||||
account: ben,
|
||||
assistant: 'claude',
|
||||
),
|
||||
endpoints.assistantOperator.revoke(asOlga(), account: ben),
|
||||
_refusedForOperator(OperatorRefusal.notFound),
|
||||
);
|
||||
});
|
||||
|
|
@ -1222,13 +1546,14 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('go with the person, as do their grants', () async {
|
||||
await grantAndAgree();
|
||||
test('go with the person, as do their grants and budget', () async {
|
||||
await grantAndAgree(budget: _budget(dailyCost: 1000));
|
||||
await usedBefore(DateTime.utc(2026, 10, 25));
|
||||
|
||||
await deletePerson(session, ben);
|
||||
|
||||
expect(await AssistantGrant.db.count(session), 0);
|
||||
expect(await AssistantBudget.db.count(session), 0);
|
||||
expect(await AssistantFigures.db.count(session), 0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1295,6 +1620,7 @@ final class _Clients implements LlmClient {
|
|||
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await AssistantGrant.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await AssistantBudget.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await AssistantFigures.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await deleteAll(session);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1372,7 +1372,7 @@ void main() {
|
|||
test("refuse every operator's call: no session here is an "
|
||||
"operator's", () async {
|
||||
await expectLater(
|
||||
endpoints.instanceOperator.waiting(asBen()),
|
||||
endpoints.instanceOperator.waiting(asBen(), offset: 0),
|
||||
throwsA(isA<ServerpodInsufficientAccessException>()),
|
||||
);
|
||||
expect(await endpoints.account.operating(asBen()), isNull);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import 'package:halkyon_base_server/src/generated/admission/waiting_state.dart'
|
|||
as _iuvj70hb;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_answer.dart'
|
||||
as _i07tvte4;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_grant_overview.dart'
|
||||
as _ily61o76;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_grantee.dart'
|
||||
as _ixuffldn;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_limits.dart'
|
||||
as _imik1w54;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_month_figures.dart'
|
||||
as _i5c40jr1;
|
||||
import 'package:halkyon_base_server/src/generated/assistant/assistant_question.dart'
|
||||
|
|
@ -49,6 +51,8 @@ import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
|
|||
as _ii64ejy6;
|
||||
import 'package:halkyon_base_server/src/generated/operator/account_overview.dart'
|
||||
as _iiuvgbjo;
|
||||
import 'package:halkyon_base_server/src/generated/operator/operator_counts.dart'
|
||||
as _i5bpu4l1;
|
||||
import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
|
||||
as _ip34xal6;
|
||||
import 'package:halkyon_base_server/src/generated/password/authenticator_enrolment.dart'
|
||||
|
|
@ -601,21 +605,26 @@ class _AssistantOperatorEndpoint {
|
|||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_ily61o76.AssistantGrantOverview>> grants(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
_ida.Future<List<_ixuffldn.AssistantGrantee>> grantees(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String startingWith,
|
||||
required int offset,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'assistantOperator',
|
||||
method: 'grants',
|
||||
method: 'grantees',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'assistantOperator',
|
||||
methodName: 'grants',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
methodName: 'grantees',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'startingWith': startingWith,
|
||||
'offset': offset,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
@ -623,7 +632,38 @@ class _AssistantOperatorEndpoint {
|
|||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_ily61o76.AssistantGrantOverview>>);
|
||||
as _ida.Future<List<_ixuffldn.AssistantGrantee>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_ixuffldn.AssistantGrantee>> granteesOf(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required List<_is.UuidValue> accounts,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'assistantOperator',
|
||||
method: 'granteesOf',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'assistantOperator',
|
||||
methodName: 'granteesOf',
|
||||
parameters: _ist.testObjectToJson({'accounts': accounts}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_ixuffldn.AssistantGrantee>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
|
|
@ -633,10 +673,9 @@ class _AssistantOperatorEndpoint {
|
|||
|
||||
_ida.Future<void> grant(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
required List<_is.UuidValue> accounts,
|
||||
required List<String> assistants,
|
||||
required _imik1w54.AssistantLimits budget,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -649,11 +688,47 @@ class _AssistantOperatorEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'assistantOperator',
|
||||
methodName: 'grant',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'accounts': accounts,
|
||||
'assistants': assistants,
|
||||
'budget': budget,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> change(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue account,
|
||||
required List<String> assistants,
|
||||
required _imik1w54.AssistantLimits budget,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'assistantOperator',
|
||||
method: 'change',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'assistantOperator',
|
||||
methodName: 'change',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'account': account,
|
||||
'assistant': assistant,
|
||||
'dailyLimit': dailyLimit,
|
||||
'monthlyLimit': monthlyLimit,
|
||||
'assistants': assistants,
|
||||
'budget': budget,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
@ -673,7 +748,6 @@ class _AssistantOperatorEndpoint {
|
|||
_ida.Future<void> revoke(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue account,
|
||||
required String assistant,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -686,10 +760,7 @@ class _AssistantOperatorEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'assistantOperator',
|
||||
methodName: 'revoke',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'account': account,
|
||||
'assistant': assistant,
|
||||
}),
|
||||
parameters: _ist.testObjectToJson({'account': account}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
@ -1283,9 +1354,40 @@ class _InstanceOperatorEndpoint {
|
|||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<List<_iiuvgbjo.AccountOverview>> waiting(
|
||||
_ida.Future<_i5bpu4l1.OperatorCounts> counts(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'instanceOperator',
|
||||
method: 'counts',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'instanceOperator',
|
||||
methodName: 'counts',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_i5bpu4l1.OperatorCounts>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_iiuvgbjo.AccountOverview>> waiting(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required int offset,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
|
|
@ -1297,7 +1399,7 @@ class _InstanceOperatorEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'instanceOperator',
|
||||
methodName: 'waiting',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
parameters: _ist.testObjectToJson({'offset': offset}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
@ -1378,6 +1480,7 @@ class _InstanceOperatorEndpoint {
|
|||
_ida.Future<List<_iiuvgbjo.AccountOverview>> accounts(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String startingWith,
|
||||
required int offset,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -1390,7 +1493,10 @@ class _InstanceOperatorEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'instanceOperator',
|
||||
methodName: 'accounts',
|
||||
parameters: _ist.testObjectToJson({'startingWith': startingWith}),
|
||||
parameters: _ist.testObjectToJson({
|
||||
'startingWith': startingWith,
|
||||
'offset': offset,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
|
|||
|
|
@ -123,6 +123,36 @@ void main() {
|
|||
expect(prices.costMicros(inputTokens: 1000, outputTokens: 200), 6100);
|
||||
});
|
||||
|
||||
test("the effort: the operator's default and highest level, by "
|
||||
"Anthropic's names, whatever their case (D63)", () {
|
||||
final assistant = assistantsFrom(
|
||||
{
|
||||
...claude,
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'Medium',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'xhigh',
|
||||
}.get,
|
||||
).all.single;
|
||||
expect(assistant.effort, LlmEffort.medium);
|
||||
expect(assistant.efforts, [
|
||||
LlmEffort.low,
|
||||
LlmEffort.medium,
|
||||
LlmEffort.high,
|
||||
LlmEffort.xhigh,
|
||||
]);
|
||||
});
|
||||
|
||||
test("a default alone is the highest too; none: the model's own, and "
|
||||
'none to choose', () {
|
||||
final fixed = assistantsFrom(
|
||||
{...claude, 'ASSISTANT_CLAUDE_EFFORT': 'high'}.get,
|
||||
).all.single;
|
||||
expect(fixed.effort, LlmEffort.high);
|
||||
expect(fixed.efforts, [LlmEffort.low, LlmEffort.medium, LlmEffort.high]);
|
||||
final own = assistantsFrom(claude.get).all.single;
|
||||
expect(own.effort, isNull);
|
||||
expect(own.efforts, isEmpty);
|
||||
});
|
||||
|
||||
group('refuse what would go wrong later, so the server stops', () {
|
||||
test('an ID that is no name of a variable', () {
|
||||
expect(
|
||||
|
|
@ -211,6 +241,63 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('an effort that is no level, naming those there are', () {
|
||||
expect(
|
||||
() => assistantsFrom(
|
||||
{...claude, 'ASSISTANT_CLAUDE_EFFORT': 'minimal'}.get,
|
||||
),
|
||||
refusedNaming('low, medium, high, xhigh, max'),
|
||||
);
|
||||
});
|
||||
|
||||
test('a highest effort without a default, or below it', () {
|
||||
expect(
|
||||
() => assistantsFrom(
|
||||
{...claude, 'ASSISTANT_CLAUDE_MAX_EFFORT': 'high'}.get,
|
||||
),
|
||||
refusedNaming('ASSISTANT_CLAUDE_EFFORT'),
|
||||
);
|
||||
expect(
|
||||
() => assistantsFrom(
|
||||
{
|
||||
...claude,
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'high',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'low',
|
||||
}.get,
|
||||
),
|
||||
refusedNaming('ASSISTANT_CLAUDE_MAX_EFFORT'),
|
||||
);
|
||||
});
|
||||
|
||||
test("an effort the provider takes not: Mistral's none, Gemini's "
|
||||
'above high', () {
|
||||
final atMistral = {
|
||||
...claude,
|
||||
'ASSISTANT_CLAUDE_PROTOCOL': 'openai',
|
||||
'ASSISTANT_CLAUDE_BASE_URL': 'https://api.mistral.ai/v1',
|
||||
};
|
||||
expect(
|
||||
() => assistantsFrom(
|
||||
{...atMistral, 'ASSISTANT_CLAUDE_EFFORT': 'low'}.get,
|
||||
),
|
||||
refusedNaming('ASSISTANT_CLAUDE_EFFORT'),
|
||||
);
|
||||
expect(assistantsFrom(atMistral.get).all.single.efforts, isEmpty);
|
||||
expect(
|
||||
() => assistantsFrom(
|
||||
{
|
||||
...claude,
|
||||
'ASSISTANT_CLAUDE_PROTOCOL': 'openai',
|
||||
'ASSISTANT_CLAUDE_BASE_URL':
|
||||
'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
'ASSISTANT_CLAUDE_EFFORT': 'high',
|
||||
'ASSISTANT_CLAUDE_MAX_EFFORT': 'xhigh',
|
||||
}.get,
|
||||
),
|
||||
refusedNaming('ASSISTANT_CLAUDE_MAX_EFFORT'),
|
||||
);
|
||||
});
|
||||
|
||||
test('a switch neither on nor off', () {
|
||||
expect(
|
||||
() =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue