-
Notifications
You must be signed in to change notification settings - Fork 12
feat(providers): show each agent the share of a picked set it can run #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
497e3cb
74be545
3bf358b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // @vitest-environment jsdom | ||
|
|
||
| import type { AgentKind } from '@linkcode/schema'; | ||
| import { cleanup, render, screen } from '@testing-library/react'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| import type { | ||
| ProviderAccountDetailViewModel, | ||
| ProviderAgentViewModel, | ||
| } from '../providers/account-detail'; | ||
| import { AccountDetail } from '../providers/account-detail'; | ||
|
|
||
| /** Names each value, so an assertion says which number it expected where. */ | ||
| function passthrough(key: string, values?: Record<string, unknown>): string { | ||
| if (!values) return key; | ||
| const named = Object.entries(values).map(([name, value]) => `${name}=${String(value)}`); | ||
| return `${key}:${named.join(',')}`; | ||
| } | ||
|
|
||
| vi.mock('use-intl', () => ({ useTranslations: () => passthrough })); | ||
|
|
||
| afterEach(cleanup); | ||
|
|
||
| const REACHABLE_PATTERN = /modelsReachable/; | ||
|
|
||
| function detail(agents: ProviderAgentViewModel[]): ProviderAccountDetailViewModel { | ||
| const bound = agents.reduce<AgentKind[]>((kinds, agent) => { | ||
| if (agent.enabled) kinds.push(agent.kind); | ||
| return kinds; | ||
| }, []); | ||
| return { | ||
| id: 'acc_gw', | ||
| label: 'LinkCode Gateway', | ||
| credential: { kind: 'secret', type: 'auth-token', value: 'lc-secret', maskedValue: 'lc-…ret' }, | ||
| agents, | ||
| boundAgents: bound, | ||
| enabledAgentCount: bound.length, | ||
| availableAgentCount: agents.filter(({ tier }) => tier !== 'unavailable').length, | ||
| }; | ||
| } | ||
|
|
||
| function renderDetail(agent: ProviderAgentViewModel): void { | ||
| render( | ||
| <AccountDetail | ||
| account={detail([agent])} | ||
| busy={false} | ||
| onSetAccountEnabled={vi.fn()} | ||
| onEdit={vi.fn()} | ||
| onRemove={vi.fn()} | ||
| />, | ||
| ); | ||
| } | ||
|
|
||
| describe('AccountDetail agent rows', () => { | ||
| it('names the share it was handed, each number under its own placeholder', () => { | ||
| renderDetail({ | ||
| kind: 'codex', | ||
| tier: 'native', | ||
| enabled: true, | ||
| status: { kind: 'model-shortfall', picked: 3, reachable: 2 }, | ||
| }); | ||
| expect(screen.getByText('modelsReachable:picked=3,reachable=2')).toBeTruthy(); | ||
| }); | ||
|
|
||
| // Which one of these a row gets is the view model's call (see the workbench view tests); the | ||
| // row's own rule is only that an absent status says nothing at all. | ||
| it('says nothing when it was handed no status', () => { | ||
| renderDetail({ kind: 'codex', tier: 'native', enabled: true }); | ||
| expect(screen.queryByText(REACHABLE_PATTERN)).toBeNull(); | ||
| }); | ||
|
|
||
| it('says why an enabled agent offers nothing instead of leaving the switch to imply it', () => { | ||
| renderDetail({ | ||
| kind: 'codex', | ||
| tier: 'native', | ||
| enabled: true, | ||
| status: { kind: 'no-reachable-model' }, | ||
| }); | ||
| expect(screen.getByText('noReachableModel')).toBeTruthy(); | ||
| // Zero reachable is the status' story; a "0 of 3" ratio beside it would say it twice. | ||
| expect(screen.queryByText(REACHABLE_PATTERN)).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,14 +34,19 @@ export type ProviderAgentStatus = | |
| | { kind: 'unavailable-oauth'; agent: AgentKind } | ||
| | { kind: 'unavailable-endpoint-incomplete' } | ||
| | { kind: 'unavailable-protocol' } | ||
| | { kind: 'no-reachable-model' } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variant falsifies the doc comment on
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rewrite in 3bf358b fixes the false "Absent means enabled and available" claim and adds the shortfall, but its enumeration ("a reason it cannot be, or is not, on, or a picked set it can run only part of") still omits
|
||
| | { kind: 'model-shortfall'; picked: number; reachable: number } | ||
| | { kind: 'disabled' }; | ||
|
|
||
| /** One agent row in an account's dialog: whether this account's models are offered to that agent. | ||
| * That is the whole state — nothing here is a default, and the switch says it without help. */ | ||
| export interface ProviderAgentViewModel { | ||
| kind: AgentKind; | ||
| tier: 'native' | 'translate' | 'unavailable'; | ||
| /** Only a reason the row cannot be, or is not, on. Absent means enabled and available. */ | ||
| /** The one thing worth saying about this row — a reason it cannot be, or is not, on, or a picked | ||
| * set it can run only part of. Absent means nothing to say. One field rather than several, | ||
| * because these never stack: the view model picks which one applies, so the row cannot render | ||
| * "off" and "2 of 3 models" as if both were the news. */ | ||
| status?: ProviderAgentStatus; | ||
| enabled: boolean; | ||
| } | ||
|
|
@@ -314,6 +319,10 @@ function agentStatusLabel( | |
| return t('unavailableEndpointIncomplete'); | ||
| case 'unavailable-protocol': | ||
| return t('unavailableProtocol'); | ||
| case 'no-reachable-model': | ||
| return t('noReachableModel'); | ||
| case 'model-shortfall': | ||
| return t('modelsReachable', { picked: status.picked, reachable: status.reachable }); | ||
| case 'disabled': | ||
| return t('accountDisabled'); | ||
| default: | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The display gate is
reachable > 0 && reachable < picked, soreachable === 1is the common shortfall and the string renders "1 of 2 selected models work with this agent". This file already uses ICU plurals in ~18 places, so the agreement is worth fixing; rewording sidesteps the plural form entirely, or keep the count-first phrasing with{reachable, plural, one {works} other {work}}.