mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 08:33:03 +00:00
Add branch-coverage tests for three changed frontend model modules
containerUpdateBadgeModel gained update icon and button helpers since its prior branchcov test, so this covers the three previously-uncovered functions and their state arms (idle, confirming, updating, success, error and the default). aiProviderPresentation route-label formatting had fourteen uncovered branches, now exercised through the four exported entry points. dockerIdentityConflict conflict collection reaches full branch coverage including the hostnames fallback and the name fallback chain. All three are new test-only files under existing __tests__ directories. No source changed. Each was confirmed with vitest and type-check, and measured to raise branch or function coverage over the pre-existing suite.
This commit is contained in:
+482
@@ -0,0 +1,482 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
hasContainerUpdate,
|
||||
hasContainerUpdateError,
|
||||
hasContainerUpdateCurrent,
|
||||
getContainerUpdateErrorTooltip,
|
||||
getContainerUpdateCurrentTooltip,
|
||||
getContainerUpdateBadgeTooltip,
|
||||
getUpdateIconTooltip,
|
||||
getUpdateButtonClass,
|
||||
getUpdateButtonLabel,
|
||||
getUpdateButtonTooltip,
|
||||
type UpdateState,
|
||||
} from '@/components/shared/containerUpdateBadgeModel';
|
||||
import type { DockerContainerUpdateStatus } from '@/types/api';
|
||||
|
||||
// Branch-coverage suite for every exported helper in
|
||||
// containerUpdateBadgeModel.ts. Each `it()` pins a concrete return value
|
||||
// (exact string / boolean / class string) so it locks the precise branch
|
||||
// taken — no truthiness-only checks, no source-string echoes, no snapshots.
|
||||
//
|
||||
// `UPDATE_BUTTON_BASE_CLASS` is a module-private (non-exported) const, so its
|
||||
// literal value is inlined here as `BASE_CLASS` to assert the full produced
|
||||
// class string verbatim. This is an observable runtime value, not an
|
||||
// implementation-detail import.
|
||||
const BASE_CLASS =
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium transition-all';
|
||||
|
||||
// A digest long enough that slice(0, 12) and slice(0, 19) both truncate.
|
||||
// Pre-computed truncations (verified, not recomputed via slice in the test):
|
||||
// slice(0, 12) -> 'sha256:01234'
|
||||
// slice(0, 19) -> 'sha256:0123456789ab'
|
||||
const LONG_DIGEST = 'sha256:0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
function makeStatus(
|
||||
overrides: Partial<DockerContainerUpdateStatus> = {},
|
||||
): DockerContainerUpdateStatus {
|
||||
return {
|
||||
updateAvailable: false,
|
||||
lastChecked: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('containerUpdateBadgeModel.branchcov', () => {
|
||||
describe('hasContainerUpdate', () => {
|
||||
it('returns true when updateStatus.updateAvailable === true', () => {
|
||||
expect(hasContainerUpdate(makeStatus({ updateAvailable: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when updateStatus.updateAvailable === false', () => {
|
||||
expect(hasContainerUpdate(makeStatus({ updateAvailable: false }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when updateStatus is undefined (optional-chain arm)', () => {
|
||||
expect(hasContainerUpdate(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when updateAvailable is absent (loose-equality undefined arm)', () => {
|
||||
// `updateStatus?.updateAvailable === true` uses strict equality, so a
|
||||
// missing field (undefined) must NOT be treated as an update.
|
||||
const partial = { lastChecked: 1 } as DockerContainerUpdateStatus;
|
||||
expect(hasContainerUpdate(partial)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasContainerUpdateError', () => {
|
||||
it('returns true when updateStatus.error is a non-empty string', () => {
|
||||
expect(hasContainerUpdateError(makeStatus({ error: 'boom' }))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when error is absent (Boolean(undefined) arm)', () => {
|
||||
expect(hasContainerUpdateError(makeStatus({}))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when updateStatus is undefined (optional-chain arm)', () => {
|
||||
expect(hasContainerUpdateError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when error is an empty string (Boolean("") arm)', () => {
|
||||
// Boolean('') === false — pins the falsy-string edge of Boolean().
|
||||
expect(hasContainerUpdateError(makeStatus({ error: '' }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasContainerUpdateCurrent', () => {
|
||||
it('returns true when updateAvailable === false and there is no error', () => {
|
||||
expect(hasContainerUpdateCurrent(makeStatus({ updateAvailable: false }))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when updateAvailable === true (left operand of && is false)', () => {
|
||||
expect(hasContainerUpdateCurrent(makeStatus({ updateAvailable: true }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when an error is present alongside updateAvailable === false (right operand of && is false)', () => {
|
||||
expect(
|
||||
hasContainerUpdateCurrent(makeStatus({ updateAvailable: false, error: 'x' })),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when updateStatus is undefined (optional-chain / left-operand arm)', () => {
|
||||
expect(hasContainerUpdateCurrent(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when updateAvailable is absent (strict === false fails)', () => {
|
||||
const partial = { lastChecked: 1 } as DockerContainerUpdateStatus;
|
||||
expect(hasContainerUpdateCurrent(partial)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContainerUpdateErrorTooltip', () => {
|
||||
it('uses the provided error string (|| left arm)', () => {
|
||||
expect(getContainerUpdateErrorTooltip(makeStatus({ error: 'registry 500' }))).toBe(
|
||||
'Update check failed: registry 500',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to "Unknown error" when error is undefined (|| right arm)', () => {
|
||||
expect(getContainerUpdateErrorTooltip(makeStatus({}))).toBe(
|
||||
'Update check failed: Unknown error',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to "Unknown error" when updateStatus itself is undefined', () => {
|
||||
expect(getContainerUpdateErrorTooltip(undefined)).toBe(
|
||||
'Update check failed: Unknown error',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to "Unknown error" when error is an empty string (|| right arm, falsy string)', () => {
|
||||
// `'' || 'Unknown error'` evaluates to the right operand.
|
||||
expect(getContainerUpdateErrorTooltip(makeStatus({ error: '' }))).toBe(
|
||||
'Update check failed: Unknown error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContainerUpdateCurrentTooltip', () => {
|
||||
it('returns the bare "Image is current" message when currentDigest is absent (early-return arm)', () => {
|
||||
expect(getContainerUpdateCurrentTooltip(makeStatus({}))).toBe('Image is current');
|
||||
});
|
||||
|
||||
it('returns the bare "Image is current" message when updateStatus is undefined', () => {
|
||||
expect(getContainerUpdateCurrentTooltip(undefined)).toBe('Image is current');
|
||||
});
|
||||
|
||||
it('returns the bare "Image is current" message when currentDigest is an empty string', () => {
|
||||
// `!updateStatus?.currentDigest` is true for '' — pins the falsy-string edge.
|
||||
expect(getContainerUpdateCurrentTooltip(makeStatus({ currentDigest: '' }))).toBe(
|
||||
'Image is current',
|
||||
);
|
||||
});
|
||||
|
||||
it('appends a 12-char digest preview when currentDigest is present', () => {
|
||||
expect(
|
||||
getContainerUpdateCurrentTooltip(makeStatus({ currentDigest: LONG_DIGEST })),
|
||||
).toBe('Image is current\nDigest: sha256:01234...');
|
||||
});
|
||||
|
||||
it('uses the full digest when it is shorter than 12 chars (slice no-op arm)', () => {
|
||||
expect(
|
||||
getContainerUpdateCurrentTooltip(makeStatus({ currentDigest: 'abc' })),
|
||||
).toBe('Image is current\nDigest: abc...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContainerUpdateBadgeTooltip', () => {
|
||||
it('renders "unknown" for both digests when updateStatus is undefined', () => {
|
||||
// getDigestPreview(undefined, 19) -> 'unknown'
|
||||
expect(getContainerUpdateBadgeTooltip(undefined)).toBe(
|
||||
'Image update available\nCurrent: unknown...\nLatest: unknown...',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders "unknown" for both digests when neither digest is set', () => {
|
||||
expect(getContainerUpdateBadgeTooltip(makeStatus({}))).toBe(
|
||||
'Image update available\nCurrent: unknown...\nLatest: unknown...',
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates both digests to 19 chars when both are present', () => {
|
||||
expect(
|
||||
getContainerUpdateBadgeTooltip(
|
||||
makeStatus({ currentDigest: LONG_DIGEST, latestDigest: LONG_DIGEST }),
|
||||
),
|
||||
).toBe(
|
||||
'Image update available\nCurrent: sha256:0123456789ab...\nLatest: sha256:0123456789ab...',
|
||||
);
|
||||
});
|
||||
|
||||
it('coerces an empty-string digest to "unknown" (|| right arm of getDigestPreview)', () => {
|
||||
// Empty string is falsy: `(''.slice(0,19)) || 'unknown'` -> 'unknown'.
|
||||
expect(
|
||||
getContainerUpdateBadgeTooltip(
|
||||
makeStatus({ currentDigest: '', latestDigest: '' }),
|
||||
),
|
||||
).toBe(
|
||||
'Image update available\nCurrent: unknown...\nLatest: unknown...',
|
||||
);
|
||||
});
|
||||
|
||||
it('mixes a present currentDigest with an absent latestDigest', () => {
|
||||
expect(
|
||||
getContainerUpdateBadgeTooltip(
|
||||
makeStatus({ currentDigest: LONG_DIGEST }),
|
||||
),
|
||||
).toBe(
|
||||
'Image update available\nCurrent: sha256:0123456789ab...\nLatest: unknown...',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdateIconTooltip', () => {
|
||||
it('returns the short message when updateStatus is undefined (early-return arm)', () => {
|
||||
expect(getUpdateIconTooltip(undefined)).toBe('Image update available');
|
||||
});
|
||||
|
||||
it('returns the full multi-line message with 12-char previews when updateStatus is present', () => {
|
||||
expect(
|
||||
getUpdateIconTooltip(
|
||||
makeStatus({ currentDigest: LONG_DIGEST, latestDigest: LONG_DIGEST }),
|
||||
),
|
||||
).toBe(
|
||||
'Update available\nCurrent: sha256:01234...\nLatest: sha256:01234...',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "unknown" previews when updateStatus is present but digests are absent', () => {
|
||||
// Exercises the non-undefined updateStatus arm with both digests missing.
|
||||
expect(getUpdateIconTooltip(makeStatus({}))).toBe(
|
||||
'Update available\nCurrent: unknown...\nLatest: unknown...',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdateButtonClass', () => {
|
||||
it('returns the amber "confirming" classes', () => {
|
||||
expect(getUpdateButtonClass('confirming')).toBe(
|
||||
[
|
||||
BASE_CLASS,
|
||||
'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300 cursor-pointer hover:bg-amber-200 dark:hover:bg-amber-900',
|
||||
].join(' '),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the blue cursor-wait "updating" classes', () => {
|
||||
expect(getUpdateButtonClass('updating')).toBe(
|
||||
[
|
||||
BASE_CLASS,
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 cursor-wait',
|
||||
].join(' '),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the green "success" classes', () => {
|
||||
expect(getUpdateButtonClass('success')).toBe(
|
||||
[BASE_CLASS, 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'].join(' '),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the red cursor-help "error" classes', () => {
|
||||
expect(getUpdateButtonClass('error')).toBe(
|
||||
[
|
||||
BASE_CLASS,
|
||||
'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 cursor-help',
|
||||
].join(' '),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the blue cursor-pointer "idle" classes for the default arm', () => {
|
||||
// The literal 'idle' value falls through to `default`.
|
||||
expect(getUpdateButtonClass('idle')).toBe(
|
||||
[
|
||||
BASE_CLASS,
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900',
|
||||
].join(' '),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes any unknown state value to the default arm', () => {
|
||||
// Cast a bogus value to prove the default branch, not just 'idle'.
|
||||
const bogus = 'not-a-state' as unknown as UpdateState;
|
||||
expect(getUpdateButtonClass(bogus)).toBe(
|
||||
[
|
||||
BASE_CLASS,
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900',
|
||||
].join(' '),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdateButtonLabel', () => {
|
||||
describe('settingsLoaded = false (early-return arm: always "Update")', () => {
|
||||
it.each<UpdateState>(['idle', 'confirming', 'updating', 'success', 'error'])(
|
||||
'returns "Update" for state %s when settings are not loaded',
|
||||
(state) => {
|
||||
expect(getUpdateButtonLabel(state, false)).toBe('Update');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('settingsLoaded = true (switch arms)', () => {
|
||||
it('returns "Update" for the idle/default arm', () => {
|
||||
expect(getUpdateButtonLabel('idle', true)).toBe('Update');
|
||||
});
|
||||
|
||||
it('returns "Confirm?" for the confirming arm', () => {
|
||||
expect(getUpdateButtonLabel('confirming', true)).toBe('Confirm?');
|
||||
});
|
||||
|
||||
it('returns "Updating..." for the updating arm', () => {
|
||||
expect(getUpdateButtonLabel('updating', true)).toBe('Updating...');
|
||||
});
|
||||
|
||||
it('returns "Queued!" for the success arm', () => {
|
||||
expect(getUpdateButtonLabel('success', true)).toBe('Queued!');
|
||||
});
|
||||
|
||||
it('returns "Failed" for the error arm', () => {
|
||||
expect(getUpdateButtonLabel('error', true)).toBe('Failed');
|
||||
});
|
||||
|
||||
it('routes an unknown state value to the default "Update" arm', () => {
|
||||
const bogus = 'not-a-state' as unknown as UpdateState;
|
||||
expect(getUpdateButtonLabel(bogus, true)).toBe('Update');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdateButtonTooltip', () => {
|
||||
describe('confirming state', () => {
|
||||
it('returns the static confirm prompt', () => {
|
||||
expect(getUpdateButtonTooltip({ state: 'confirming' })).toBe(
|
||||
'Click again to confirm update',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('success state', () => {
|
||||
it('returns the static success message', () => {
|
||||
expect(getUpdateButtonTooltip({ state: 'success' })).toBe(
|
||||
'✓ Update completed successfully!',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updating state', () => {
|
||||
it('reports 0s elapsed with the default step when storeState is absent (elapsed=0, <=60 arm)', () => {
|
||||
expect(getUpdateButtonTooltip({ state: 'updating', now: 100000 })).toBe(
|
||||
'Processing... (0s)',
|
||||
);
|
||||
});
|
||||
|
||||
it('computes elapsed seconds from storeState.startedAt and uses storeState.message (<=60 arm)', () => {
|
||||
// now=100000, startedAt=95000 -> elapsed = round(5000/1000) = 5
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'updating',
|
||||
now: 100000,
|
||||
storeState: { startedAt: 95000, message: 'Pulling layers' },
|
||||
}),
|
||||
).toBe('Pulling layers (5s)');
|
||||
});
|
||||
|
||||
it('falls back to "Processing..." when storeState is present but has no message', () => {
|
||||
// step = storeState?.message || 'Processing...' -> right operand.
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'updating',
|
||||
now: 100000,
|
||||
storeState: { startedAt: 100000 },
|
||||
}),
|
||||
).toBe('Processing... (0s)');
|
||||
});
|
||||
|
||||
it('formats as "Xm Ys" once elapsed exceeds 60 seconds (>60 arm)', () => {
|
||||
// now=1000000, startedAt=0 -> elapsed = 1000 -> floor(1000/60)=16, 1000%60=40
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'updating',
|
||||
now: 1000000,
|
||||
storeState: { startedAt: 0, message: 'Extracting' },
|
||||
}),
|
||||
).toBe('Extracting (16m 40s)');
|
||||
});
|
||||
|
||||
it('pins the exact 60s boundary as still the seconds format (<=60 arm, not >60)', () => {
|
||||
// elapsed == 60 is NOT > 60, so the "(60s)" arm must be taken.
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'updating',
|
||||
now: 60000,
|
||||
storeState: { startedAt: 0, message: 'Extracting' },
|
||||
}),
|
||||
).toBe('Extracting (60s)');
|
||||
});
|
||||
|
||||
it('uses Date.now() for `now` when options.now is omitted (?? right arm)', () => {
|
||||
// Drive the `options.now ?? Date.now()` right arm deterministically by
|
||||
// stubbing Date.now(); startedAt is anchored to the same stubbed value
|
||||
// so elapsed is pinned to 7 seconds.
|
||||
const spy = vi.spyOn(Date, 'now').mockReturnValue(7000);
|
||||
try {
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'updating',
|
||||
storeState: { startedAt: 0, message: 'Restarting' },
|
||||
}),
|
||||
).toBe('Restarting (7s)');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('error state', () => {
|
||||
it('prefers storeState.message when present', () => {
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'error',
|
||||
storeState: { startedAt: 0, message: 'image not found' },
|
||||
errorMessage: 'fallback',
|
||||
}),
|
||||
).toBe('✗ Update failed: image not found');
|
||||
});
|
||||
|
||||
it('falls back to errorMessage when storeState is absent', () => {
|
||||
expect(
|
||||
getUpdateButtonTooltip({ state: 'error', errorMessage: 'timeout' }),
|
||||
).toBe('✗ Update failed: timeout');
|
||||
});
|
||||
|
||||
it('falls back to errorMessage when storeState is present but has no message', () => {
|
||||
// storeState?.message is undefined -> next operand (errorMessage).
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'error',
|
||||
storeState: { startedAt: 0 },
|
||||
errorMessage: 'denied',
|
||||
}),
|
||||
).toBe('✗ Update failed: denied');
|
||||
});
|
||||
|
||||
it('falls back to "Unknown error" when neither storeState.message nor errorMessage is set', () => {
|
||||
expect(getUpdateButtonTooltip({ state: 'error' })).toBe(
|
||||
'✗ Update failed: Unknown error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('idle/default state', () => {
|
||||
it('returns "Update container" when updateStatus is absent (early-return arm)', () => {
|
||||
expect(getUpdateButtonTooltip({ state: 'idle' })).toBe('Update container');
|
||||
});
|
||||
|
||||
it('returns the digest-preview message when updateStatus is present', () => {
|
||||
expect(
|
||||
getUpdateButtonTooltip({
|
||||
state: 'idle',
|
||||
updateStatus: makeStatus({
|
||||
currentDigest: LONG_DIGEST,
|
||||
latestDigest: LONG_DIGEST,
|
||||
}),
|
||||
}),
|
||||
).toBe(
|
||||
'Click to update\nCurrent: sha256:01234...\nLatest: sha256:01234...',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "unknown" previews when updateStatus is present but digests are absent', () => {
|
||||
expect(
|
||||
getUpdateButtonTooltip({ state: 'idle', updateStatus: makeStatus({}) }),
|
||||
).toBe('Click to update\nCurrent: unknown...\nLatest: unknown...');
|
||||
});
|
||||
|
||||
it('routes an unknown state value (with no updateStatus) to the default early-return arm', () => {
|
||||
const bogus = 'not-a-state' as unknown as UpdateState;
|
||||
expect(getUpdateButtonTooltip({ state: bogus })).toBe('Update container');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { collectIdentityConflictHosts } from '../dockerIdentityConflict';
|
||||
|
||||
// Minimal docker-host Resource factory. Only the fields the function under test
|
||||
// reads are populated; the partial fixture is cast to Resource (the same pattern
|
||||
// used by the sibling dockerIdentityConflict.test.tsx).
|
||||
const host = (overrides: Partial<Resource> = {}): Resource =>
|
||||
({
|
||||
id: 'docker-host-1',
|
||||
type: 'docker-host',
|
||||
name: 'docker-host-1',
|
||||
...overrides,
|
||||
}) as Resource;
|
||||
|
||||
describe('dockerIdentityConflict.branchcov', () => {
|
||||
describe('collectIdentityConflictHosts — input shape branches', () => {
|
||||
it('returns an empty array when the input list is empty', () => {
|
||||
expect(collectIdentityConflictHosts([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns an empty array when no host has docker.identityConflict (docker absent arm)', () => {
|
||||
// `host.docker?.identityConflict` short-circuits at `docker` undefined ->
|
||||
// `!conflict` is true -> continue.
|
||||
const hosts = [
|
||||
host({ id: 'h1', name: 'h1' }),
|
||||
host({ id: 'h2', name: 'h2' }),
|
||||
];
|
||||
expect(collectIdentityConflictHosts(hosts)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns an empty array when docker is present but identityConflict is undefined', () => {
|
||||
// `docker` truthy, `docker.identityConflict` undefined -> !conflict continue.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({ id: 'h1', name: 'h1', docker: { runtime: 'docker' } }),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectIdentityConflictHosts — falsy conflict token (continue arm)', () => {
|
||||
it('skips a host whose identityConflict is explicitly null', () => {
|
||||
// null is falsy, so `!conflict` is true and the host is excluded entirely.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: {
|
||||
identityConflict: null as unknown as { hostnames: string[] },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectIdentityConflictHosts — hostname normalization branches', () => {
|
||||
it('handles identityConflict being an empty object via the hostnames ?? [] fallback', () => {
|
||||
// Truthy `{}` passes the !conflict guard; `conflict.hostnames` is
|
||||
// undefined -> ?? produces [] -> entry is emitted with an empty list.
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: { identityConflict: {} },
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([{ name: 'h1', hostnames: [] }]);
|
||||
});
|
||||
|
||||
it('trims each hostname and preserves non-empty entries verbatim', () => {
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: {
|
||||
identityConflict: { hostnames: [' clone-a ', 'clone-b'] },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
{ name: 'h1', hostnames: ['clone-a', 'clone-b'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops hostnames that trim to empty (length === 0 filter arm)', () => {
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: {
|
||||
identityConflict: { hostnames: ['clone-a', ' ', '', '\t'] },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([{ name: 'h1', hostnames: ['clone-a'] }]);
|
||||
});
|
||||
|
||||
it('returns an empty hostnames list when every hostname is whitespace-only', () => {
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: {
|
||||
identityConflict: { hostnames: [' ', '', ' \t '] },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([{ name: 'h1', hostnames: [] }]);
|
||||
});
|
||||
|
||||
it('falls back to [] when hostnames is undefined', () => {
|
||||
// `conflict.hostnames ?? []` right operand when the key is absent.
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: { identityConflict: { firstSeen: '2026-07-16T00:00:00Z' } },
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([{ name: 'h1', hostnames: [] }]);
|
||||
});
|
||||
|
||||
it('falls back to [] when hostnames is null (deliberately invalid token)', () => {
|
||||
// `hostnames` is typed optional string[]; coerce null through `as unknown`
|
||||
// to exercise the ?? arm against a non-undefined falsy value.
|
||||
const result = collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'h1',
|
||||
docker: {
|
||||
identityConflict: {
|
||||
hostnames: null as unknown as string[],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(result).toEqual([{ name: 'h1', hostnames: [] }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectIdentityConflictHosts — name resolution branches', () => {
|
||||
it('uses the trimmed resource name when it is non-empty', () => {
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'fallback-id',
|
||||
name: ' production-host ',
|
||||
docker: { identityConflict: { hostnames: ['a'] } },
|
||||
}),
|
||||
]),
|
||||
).toEqual([{ name: 'production-host', hostnames: ['a'] }]);
|
||||
});
|
||||
|
||||
it('falls back to host.id when name is whitespace-only', () => {
|
||||
// `host.name?.trim()` is '' (falsy) -> `|| host.id` arm.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'docker-host-42',
|
||||
name: ' ',
|
||||
docker: { identityConflict: { hostnames: ['a'] } },
|
||||
}),
|
||||
]),
|
||||
).toEqual([{ name: 'docker-host-42', hostnames: ['a'] }]);
|
||||
});
|
||||
|
||||
it('falls back to host.id when name is undefined', () => {
|
||||
// `host.name?.trim()` is undefined (falsy) -> `|| host.id` arm.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: 'docker-host-7',
|
||||
name: undefined as unknown as string,
|
||||
docker: { identityConflict: { hostnames: ['a'] } },
|
||||
}),
|
||||
]),
|
||||
).toEqual([{ name: 'docker-host-7', hostnames: ['a'] }]);
|
||||
});
|
||||
|
||||
it('falls back to the literal "host" when both name and id are blank', () => {
|
||||
// Last `|| 'host'` fallback arm; name '' -> falsy, id '' -> falsy.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: '',
|
||||
name: '',
|
||||
docker: { identityConflict: { hostnames: ['a'] } },
|
||||
}),
|
||||
]),
|
||||
).toEqual([{ name: 'host', hostnames: ['a'] }]);
|
||||
});
|
||||
|
||||
it('falls back to the literal "host" when name is whitespace and id is empty', () => {
|
||||
// Combines the trim-to-empty arm for name with the falsy id arm.
|
||||
expect(
|
||||
collectIdentityConflictHosts([
|
||||
host({
|
||||
id: '',
|
||||
name: ' ',
|
||||
docker: { identityConflict: { hostnames: ['a'] } },
|
||||
}),
|
||||
]),
|
||||
).toEqual([{ name: 'host', hostnames: ['a'] }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectIdentityConflictHosts — multi-host collection', () => {
|
||||
it('collects every conflicting host when 2+ hosts carry identityConflict tokens', () => {
|
||||
// The function does not cross-compare hosts; it emits one entry per host
|
||||
// that carries its own identityConflict. Both hosts here are flagged.
|
||||
const hosts = [
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'host-one',
|
||||
docker: {
|
||||
identityConflict: { hostnames: ['clone-a', 'clone-b'] },
|
||||
},
|
||||
}),
|
||||
host({
|
||||
id: 'h2',
|
||||
name: 'host-two',
|
||||
docker: {
|
||||
identityConflict: { hostnames: ['clone-c'] },
|
||||
},
|
||||
}),
|
||||
];
|
||||
expect(collectIdentityConflictHosts(hosts)).toEqual([
|
||||
{ name: 'host-one', hostnames: ['clone-a', 'clone-b'] },
|
||||
{ name: 'host-two', hostnames: ['clone-c'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('excludes non-conflicting hosts from a mixed list', () => {
|
||||
// First and third hosts conflict; the middle host is healthy and must be
|
||||
// excluded while preserving the order of the conflictors.
|
||||
const hosts = [
|
||||
host({
|
||||
id: 'h1',
|
||||
name: 'conflictor-one',
|
||||
docker: {
|
||||
identityConflict: { hostnames: ['dup-a', 'dup-b'] },
|
||||
},
|
||||
}),
|
||||
host({ id: 'h2', name: 'healthy-middle' }),
|
||||
host({
|
||||
id: 'h3',
|
||||
name: 'conflictor-two',
|
||||
docker: {
|
||||
identityConflict: { hostnames: ['dup-c'] },
|
||||
},
|
||||
}),
|
||||
];
|
||||
expect(collectIdentityConflictHosts(hosts)).toEqual([
|
||||
{ name: 'conflictor-one', hostnames: ['dup-a', 'dup-b'] },
|
||||
{ name: 'conflictor-two', hostnames: ['dup-c'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves input order when emitting multiple conflicting hosts', () => {
|
||||
// The function iterates hosts in order and pushes in order, with no
|
||||
// deduplication or sorting applied.
|
||||
const hosts = [
|
||||
host({
|
||||
id: 'zeta',
|
||||
name: 'zeta',
|
||||
docker: { identityConflict: { hostnames: ['z-1'] } },
|
||||
}),
|
||||
host({
|
||||
id: 'alpha',
|
||||
name: 'alpha',
|
||||
docker: { identityConflict: { hostnames: ['a-1'] } },
|
||||
}),
|
||||
];
|
||||
expect(collectIdentityConflictHosts(hosts).map((h) => h.name)).toEqual([
|
||||
'zeta',
|
||||
'alpha',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
formatAIModelRouteLabel,
|
||||
getAIProviderDisplayName,
|
||||
getProviderFromModelId,
|
||||
isPulseOwnedLocalModelRoute,
|
||||
} from '@/utils/aiProviderPresentation';
|
||||
|
||||
// `modelIdForLabel`, `explicitModelNameForLabel`, `baseModelLabel`,
|
||||
// `transportProviderForLabel`, `titleizeRouteNamespace`, `titleizeModelToken`,
|
||||
// `titleizeModelRouteId`, `gatewayFallbackLabel`, and
|
||||
// `directProviderRouteFallbackLabel` are module-private (not exported); their
|
||||
// branches are exercised transitively through the four exported entry points
|
||||
// below, asserting on their observable outputs.
|
||||
|
||||
describe('getAIProviderDisplayName — branch coverage', () => {
|
||||
it('returns the curated display name for every known provider key (map hit arm)', () => {
|
||||
// AI_PROVIDER_DISPLAY_NAMES[provider] arm — sample across simple, multi-word,
|
||||
// parenthesised, and hyphenated values to lock the map contents.
|
||||
expect(getAIProviderDisplayName('anthropic')).toBe('Anthropic');
|
||||
expect(getAIProviderDisplayName('openai')).toBe('OpenAI');
|
||||
expect(getAIProviderDisplayName('gemini')).toBe('Google Gemini');
|
||||
expect(getAIProviderDisplayName('codex-subscription')).toBe('Codex subscription (local)');
|
||||
expect(getAIProviderDisplayName('claude-subscription')).toBe('Claude subscription (local)');
|
||||
expect(getAIProviderDisplayName('pulse')).toBe('Pulse');
|
||||
expect(getAIProviderDisplayName('together')).toBe('Together AI');
|
||||
});
|
||||
|
||||
it('falls back to the raw provider string for an unknown key (|| arm)', () => {
|
||||
// AI_PROVIDER_DISPLAY_NAMES[provider] === undefined -> || provider fallback.
|
||||
expect(getAIProviderDisplayName('not-a-real-provider')).toBe('not-a-real-provider');
|
||||
});
|
||||
|
||||
it('returns the empty string for an empty-string key (both arms collapse to "")', () => {
|
||||
// map miss -> '' || '' -> ''. Documents the boundary behaviour.
|
||||
expect(getAIProviderDisplayName('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderFromModelId — branch coverage', () => {
|
||||
it('returns the substring before the first colon when colonIndex > 0 (colon branch)', () => {
|
||||
expect(getProviderFromModelId('anthropic:claude-3-opus')).toBe('anthropic');
|
||||
expect(getProviderFromModelId('openrouter:anthropic/claude')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('pulse:local-inventory')).toBe('pulse');
|
||||
});
|
||||
|
||||
it('skips the colon branch when the colon is at index 0 (colonIndex === 0 boundary)', () => {
|
||||
// ':foo' -> colonIndex 0 -> 0 > 0 false -> falls through to keyword detection
|
||||
// -> no keyword match -> default 'ollama'.
|
||||
expect(getProviderFromModelId(':something')).toBe('ollama');
|
||||
});
|
||||
|
||||
it('maps each openrouter regex prefix variant to "openrouter"', () => {
|
||||
// Exercises every OR clause of
|
||||
// /^(openai|anthropic|google|deepseek|meta-llama|mistralai|x-ai|xai|cohere|qwen)\//
|
||||
expect(getProviderFromModelId('openai/gpt-4o')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('anthropic/claude-3')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('google/gemini-pro')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('deepseek/deepseek-chat')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('meta-llama/llama-3')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('mistralai/mistral-large')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('x-ai/grok')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('xai/grok-2')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('cohere/command-r')).toBe('openrouter');
|
||||
expect(getProviderFromModelId('qwen/qwen-2.5')).toBe('openrouter');
|
||||
});
|
||||
|
||||
it('maps each anthropic keyword (claude/opus/sonnet/haiku) to "anthropic"', () => {
|
||||
expect(getProviderFromModelId('claude-3-opus')).toBe('anthropic');
|
||||
expect(getProviderFromModelId('my-opus-model')).toBe('anthropic');
|
||||
expect(getProviderFromModelId('sonnet-4')).toBe('anthropic');
|
||||
expect(getProviderFromModelId('haiku-turbo')).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('maps each openai keyword (gpt/o1/o3/o4) to "openai"', () => {
|
||||
expect(getProviderFromModelId('gpt-4o-mini')).toBe('openai');
|
||||
expect(getProviderFromModelId('o1-preview')).toBe('openai');
|
||||
expect(getProviderFromModelId('o3-mini')).toBe('openai');
|
||||
expect(getProviderFromModelId('o4-analysis')).toBe('openai');
|
||||
});
|
||||
|
||||
it('maps a deepseek-keyword id to "deepseek"', () => {
|
||||
expect(getProviderFromModelId('deepseek-v3')).toBe('deepseek');
|
||||
});
|
||||
|
||||
it('maps a gemini-keyword id to "gemini"', () => {
|
||||
expect(getProviderFromModelId('gemini-1.5-pro')).toBe('gemini');
|
||||
});
|
||||
|
||||
it('maps a glm-keyword id to "zai"', () => {
|
||||
expect(getProviderFromModelId('glm-4.5')).toBe('zai');
|
||||
});
|
||||
|
||||
it('maps a groq-keyword id to "groq"', () => {
|
||||
expect(getProviderFromModelId('groq-llama-3')).toBe('groq');
|
||||
});
|
||||
|
||||
it('maps a mistral-keyword id to "mistral"', () => {
|
||||
expect(getProviderFromModelId('mistral-7b')).toBe('mistral');
|
||||
});
|
||||
|
||||
it('maps a cerebras-keyword id to "cerebras"', () => {
|
||||
expect(getProviderFromModelId('cerebras-llama3')).toBe('cerebras');
|
||||
});
|
||||
|
||||
it('falls back to "ollama" when no prefix/keyword matches (default arm)', () => {
|
||||
// Includes the empty-string boundary, which has no colon and no keyword.
|
||||
expect(getProviderFromModelId('random-model')).toBe('ollama');
|
||||
expect(getProviderFromModelId('')).toBe('ollama');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPulseOwnedLocalModelRoute — branch coverage', () => {
|
||||
it('returns true for the canonical pulse:local-inventory route', () => {
|
||||
expect(isPulseOwnedLocalModelRoute('pulse:local-inventory')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for the canonical pulse:mock-assistant route', () => {
|
||||
expect(isPulseOwnedLocalModelRoute('pulse:mock-assistant')).toBe(true);
|
||||
});
|
||||
|
||||
it('honours the .trim() normaliser for whitespace-padded routes', () => {
|
||||
// Boolean(LOCAL_MODEL_ROUTE_LABELS[modelId.trim()]) — verifies trim arm.
|
||||
expect(isPulseOwnedLocalModelRoute(' pulse:local-inventory ')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an unknown pulse-namespaced route (Boolean(undefined) arm)', () => {
|
||||
expect(isPulseOwnedLocalModelRoute('pulse:unknown')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a generic non-pulse id', () => {
|
||||
expect(isPulseOwnedLocalModelRoute('anthropic:claude-3')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for the empty string (misses the map)', () => {
|
||||
expect(isPulseOwnedLocalModelRoute('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatAIModelRouteLabel — branch coverage', () => {
|
||||
describe('string input — direct (non-gateway) path', () => {
|
||||
it('builds "Provider: Model" for a colon-namespaced direct-route id', () => {
|
||||
// transportProvider -> getProviderFromModelId('anthropic:...') colon arm.
|
||||
// directProviderRouteFallbackLabel returns the formatted pair.
|
||||
expect(formatAIModelRouteLabel('anthropic:claude-3-opus')).toBe('Anthropic: Claude 3 Opus');
|
||||
});
|
||||
|
||||
it('returns the raw id when there is no separator and no keyword match', () => {
|
||||
// directProviderRouteFallbackLabel returns '' (separator <= 0) -> baseModelLabel
|
||||
// -> id.split(':').pop() || id falls back to the whole id.
|
||||
expect(formatAIModelRouteLabel('random-model')).toBe('random-model');
|
||||
});
|
||||
|
||||
it('returns the curated Pulse label for a pulse-owned local route', () => {
|
||||
// directProviderRouteFallbackLabel early-returns '' for pulse-owned routes
|
||||
// -> baseModelLabel returns the LOCAL_MODEL_ROUTE_LABELS entry verbatim.
|
||||
expect(formatAIModelRouteLabel('pulse:local-inventory')).toBe('Pulse inventory');
|
||||
expect(formatAIModelRouteLabel('pulse:mock-assistant')).toBe('Pulse mock Assistant');
|
||||
});
|
||||
|
||||
it('falls back to the full id when the separator sits at the end of the id', () => {
|
||||
// directProviderRouteFallbackLabel: separator === id.length - 1 -> return ''.
|
||||
// baseModelLabel: id.split(':').pop() === '' -> || id returns the whole id.
|
||||
expect(formatAIModelRouteLabel('mistral:')).toBe('mistral:');
|
||||
});
|
||||
|
||||
it('returns the empty string for an empty-string model', () => {
|
||||
// transportProviderForLabel: id ? getProviderFromModelId(id) : '' -> ''.
|
||||
// !provider short-circuits the main guard, label resolves to ''.
|
||||
expect(formatAIModelRouteLabel('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('string input — gateway (openrouter) path', () => {
|
||||
it('builds "Upstream: Model via OpenRouter" for a slash-separated gateway payload', () => {
|
||||
// gatewayFallbackLabel: payload has slash at index > 0, both halves non-empty.
|
||||
// label does not already contain "OpenRouter" -> via suffix appended.
|
||||
expect(formatAIModelRouteLabel('openrouter:anthropic/claude-3-opus')).toBe(
|
||||
'Anthropic: Claude 3 Opus via OpenRouter',
|
||||
);
|
||||
});
|
||||
|
||||
it('titleizes known upstream namespaces (meta-llama) and 8b-style model tokens', () => {
|
||||
// Also exercises titleizeModelToken /^[0-9]+b$/ -> '8B' arm.
|
||||
expect(formatAIModelRouteLabel('openrouter:meta-llama/llama-3-8b')).toBe(
|
||||
'Meta Llama: Llama 3 8B via OpenRouter',
|
||||
);
|
||||
});
|
||||
|
||||
it('titleizes an unknown upstream namespace via the split/title-case fallback', () => {
|
||||
// titleizeRouteNamespace: not in UPSTREAM_PROVIDER_DISPLAY_NAMES -> split/->
|
||||
// map(charAt(0).upper + slice) fallback.
|
||||
expect(formatAIModelRouteLabel('openrouter:custom-ns/some-model')).toBe(
|
||||
'Custom Ns: Some Model via OpenRouter',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to baseModelLabel and appends via-suffix when payload has no slash', () => {
|
||||
// gatewayFallbackLabel: slashIndex <= 0 -> return ''.
|
||||
expect(formatAIModelRouteLabel('openrouter:gpt-4o')).toBe('gpt-4o via OpenRouter');
|
||||
});
|
||||
|
||||
it('treats slashIndex === 0 (slash at payload start) as no usable namespace', () => {
|
||||
// gatewayFallbackLabel: slashIndex <= 0 boundary at exactly 0.
|
||||
expect(formatAIModelRouteLabel('openrouter:/foo')).toBe('/foo via OpenRouter');
|
||||
});
|
||||
|
||||
it('treats slashIndex === payload.length - 1 (slash at payload end) as no usable model', () => {
|
||||
// gatewayFallbackLabel: trailing-slash boundary.
|
||||
expect(formatAIModelRouteLabel('openrouter:foo/')).toBe('foo/ via OpenRouter');
|
||||
});
|
||||
|
||||
it('suppresses the via-suffix when label already contains the provider name', () => {
|
||||
// label.toLowerCase().includes(providerName.toLowerCase()) true arm.
|
||||
expect(formatAIModelRouteLabel('openrouter:openrouter/auto')).toBe('OpenRouter: Auto');
|
||||
});
|
||||
|
||||
it('returns the bare payload when the upstream namespace normalises to empty', () => {
|
||||
// titleizeRouteNamespace('~/foo' slice 0..slashIndex = '~') -> normalised ''
|
||||
// -> !upstreamProvider -> gatewayFallbackLabel returns '' -> baseModelLabel.
|
||||
expect(formatAIModelRouteLabel('openrouter:~/foo')).toBe('~/foo via OpenRouter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('object input — transportProviderForLabel branches', () => {
|
||||
it('honours an explicit provider override that is a gateway provider', () => {
|
||||
// transportProviderForLabel: model.provider?.trim() truthy -> return provider.
|
||||
// Gateway path taken from the override alone (no colon/slash in id).
|
||||
expect(formatAIModelRouteLabel({ id: 'foo', provider: 'openrouter' })).toBe(
|
||||
'foo via OpenRouter',
|
||||
);
|
||||
});
|
||||
|
||||
it('honours an explicit provider override that is a non-gateway provider', () => {
|
||||
// Same transportProvider arm, but direct path. id has no separator so
|
||||
// directProviderRouteFallbackLabel returns '' -> baseModelLabel returns id.
|
||||
expect(formatAIModelRouteLabel({ id: 'foo', provider: 'mistral' })).toBe('foo');
|
||||
});
|
||||
|
||||
it('early-returns "" inside directProviderRouteFallbackLabel when the slice-provider is a gateway provider', () => {
|
||||
// Override is non-gateway (mistral) -> direct path. id 'openrouter:foo'
|
||||
// slices to 'openrouter' which IS in GATEWAY_MODEL_PROVIDERS -> return ''.
|
||||
expect(formatAIModelRouteLabel({ id: 'openrouter:foo', provider: 'mistral' })).toBe('foo');
|
||||
});
|
||||
|
||||
it('derives the provider from the id when no override is supplied', () => {
|
||||
// transportProviderForLabel: model.provider undefined -> falls to
|
||||
// getProviderFromModelId(id) -> 'anthropic' via the colon branch.
|
||||
expect(formatAIModelRouteLabel({ id: 'anthropic:claude-3-opus' })).toBe(
|
||||
'Anthropic: Claude 3 Opus',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "" when the object has neither provider nor a non-empty id', () => {
|
||||
// transportProviderForLabel: id ? getProviderFromModelId(id) : '' -> ''.
|
||||
expect(formatAIModelRouteLabel({ id: '' })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('object input — explicitModelNameForLabel branches', () => {
|
||||
it('uses the explicit name when it differs from id and routePayload', () => {
|
||||
// explicitModelNameForLabel returns the trimmed name -> baseModelLabel uses it.
|
||||
expect(
|
||||
formatAIModelRouteLabel({ id: 'anthropic:claude-3-opus', name: 'My Custom Claude' }),
|
||||
).toBe('My Custom Claude');
|
||||
});
|
||||
|
||||
it('drops the explicit name when it equals the whole id (name === id arm)', () => {
|
||||
// explicitModelNameForLabel: name === id -> return ''. baseModelLabel falls
|
||||
// through to id.split(':').pop().
|
||||
expect(formatAIModelRouteLabel({ id: 'foo', name: 'foo' })).toBe('foo');
|
||||
});
|
||||
|
||||
it('drops the explicit name when it equals the route payload (name === routePayload arm)', () => {
|
||||
// explicitModelNameForLabel: routePayload === name -> return ''. Falls back
|
||||
// through directProviderRouteFallbackLabel.
|
||||
expect(formatAIModelRouteLabel({ id: 'anthropic:claude', name: 'claude' })).toBe(
|
||||
'Anthropic: Claude',
|
||||
);
|
||||
});
|
||||
|
||||
it('treats a whitespace-only name as absent (name?.trim() falsy arm)', () => {
|
||||
// explicitModelNameForLabel: ' '.trim() === '' -> !name -> return ''.
|
||||
expect(formatAIModelRouteLabel({ id: 'foo', name: ' ' })).toBe('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user