mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
150 lines
6.5 KiB
TypeScript
150 lines
6.5 KiB
TypeScript
import type { AddonsRepository } from '../../../../src/db/repositories/Addons.repository';
|
|
import type { AddonsService } from '../../../../src/nest/addons/addons.service';
|
|
import { LlmConfigResolver } from '../../../../src/nest/llm-parse/llm-config.resolver';
|
|
import type { SettingsService } from '../../../../src/nest/settings/settings.service';
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const isAddonEnabled = vi.fn();
|
|
const findById = vi.fn();
|
|
|
|
// The resolver injects SettingsService — a stub instance (same behaviors as
|
|
// before the DI move). Plan 4 Task 1: the addon-row read is now
|
|
// `AddonsRepository.findById`, stubbed the same way — `config` comes back
|
|
// already-parsed (the repository's own `p.json()` column), never a JSON
|
|
// string this test has to stringify.
|
|
const getUserSettings = vi.fn(() => ({}) as Record<string, unknown>);
|
|
const getAdminUserDefaults = vi.fn(() => ({}) as Record<string, unknown>);
|
|
const getDecryptedUserSetting = vi.fn(() => null as string | null);
|
|
const settingsStub = { getUserSettings, getAdminUserDefaults, getDecryptedUserSetting } as unknown as SettingsService;
|
|
|
|
const addonsStub = { isAddonEnabled } as unknown as AddonsService;
|
|
const addonsRepoStub = { findById } as unknown as AddonsRepository;
|
|
const resolver = new LlmConfigResolver(settingsStub, addonsRepoStub, addonsStub);
|
|
|
|
function setInstanceConfig(config: Record<string, unknown> | undefined) {
|
|
findById.mockResolvedValue(config === undefined ? null : { id: 'llm_parsing', config });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
isAddonEnabled.mockReturnValue(true);
|
|
setInstanceConfig(undefined);
|
|
getUserSettings.mockReturnValue({});
|
|
getAdminUserDefaults.mockReturnValue({});
|
|
getDecryptedUserSetting.mockReturnValue(null);
|
|
});
|
|
|
|
describe('resolveLlmConfig', () => {
|
|
it('returns null when the addon is disabled', async () => {
|
|
isAddonEnabled.mockReturnValue(false);
|
|
expect(await resolver.resolve(1)).toBeNull();
|
|
// Gated BEFORE the addon-row read: a disabled addon never even queries it.
|
|
expect(findById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses instance config when present (and decrypts the key)', async () => {
|
|
setInstanceConfig({ provider: 'anthropic', model: 'claude-opus-4-8', apiKey: 'sk-plain', vision: 'on' });
|
|
expect(await resolver.resolve(1)).toEqual({
|
|
provider: 'anthropic',
|
|
model: 'claude-opus-4-8',
|
|
baseUrl: undefined,
|
|
apiKey: 'sk-plain',
|
|
vision: 'on',
|
|
});
|
|
expect(findById).toHaveBeenCalledWith('llm_parsing');
|
|
});
|
|
|
|
it('reads the instance vision setting as auto when it is missing, unknown, or only the old multimodal flag', async () => {
|
|
for (const extra of [{}, { vision: 'maybe' }, { multimodal: false }, { multimodal: true }]) {
|
|
setInstanceConfig({ provider: 'local', model: 'qwen3.5:4b', ...extra });
|
|
expect((await resolver.resolve(1))?.vision).toBe('auto');
|
|
}
|
|
setInstanceConfig({ provider: 'local', model: 'qwen3.5:4b', vision: 'off' });
|
|
expect((await resolver.resolve(1))?.vision).toBe('off');
|
|
});
|
|
|
|
it('instance config with a base URL still wins for a plain user (#1772 does not touch it)', async () => {
|
|
setInstanceConfig({ provider: 'local', model: 'nuextract', baseUrl: 'http://ollama:11434' });
|
|
expect(await resolver.resolve(7)).toMatchObject({ provider: 'local', baseUrl: 'http://ollama:11434' });
|
|
});
|
|
|
|
it('falls back to per-user config when instance config is incomplete', async () => {
|
|
setInstanceConfig({ provider: 'anthropic' }); // no model → not usable
|
|
getUserSettings.mockReturnValue({ llm_provider: 'anthropic', llm_model: 'claude-sonnet', llm_multimodal: true });
|
|
getDecryptedUserSetting.mockReturnValue('user-key');
|
|
expect(await resolver.resolve(7)).toEqual({
|
|
provider: 'anthropic',
|
|
model: 'claude-sonnet',
|
|
baseUrl: undefined,
|
|
apiKey: 'user-key',
|
|
vision: 'on',
|
|
});
|
|
expect(getDecryptedUserSetting).toHaveBeenCalledWith(7, 'llm_api_key');
|
|
});
|
|
|
|
it('returns null when neither instance nor user config is usable', async () => {
|
|
getUserSettings.mockReturnValue({ llm_provider: 'openai' }); // no model
|
|
expect(await resolver.resolve(1)).toBeNull();
|
|
});
|
|
|
|
it('returns null when the addon row has no config at all', async () => {
|
|
setInstanceConfig(undefined);
|
|
expect(await resolver.resolve(1)).toBeNull();
|
|
});
|
|
|
|
// #1772: the endpoint is instance configuration, so it may only come from an
|
|
// admin-controlled source, whoever is asking.
|
|
it('#1772: picking local personally gets no config at all (no silent reroute)', async () => {
|
|
getUserSettings.mockReturnValue({
|
|
llm_provider: 'local',
|
|
llm_model: 'nuextract',
|
|
llm_base_url: 'http://192.168.1.5:11434',
|
|
});
|
|
expect(await resolver.resolve(7)).toBeNull();
|
|
});
|
|
|
|
it('#1772: a personal OpenAI config survives but loses its own base URL', async () => {
|
|
getUserSettings.mockReturnValue({
|
|
llm_provider: 'openai',
|
|
llm_model: 'gpt-4o-mini',
|
|
llm_base_url: 'http://192.168.1.5:11434',
|
|
});
|
|
getDecryptedUserSetting.mockReturnValue('sk-user');
|
|
expect(await resolver.resolve(7)).toEqual({
|
|
provider: 'openai',
|
|
model: 'gpt-4o-mini',
|
|
baseUrl: undefined,
|
|
apiKey: 'sk-user',
|
|
vision: 'off',
|
|
});
|
|
});
|
|
|
|
it('#1772: an admin-set instance default endpoint applies to everyone', async () => {
|
|
// getUserSettings merges the admin defaults in; getAdminUserDefaults is the
|
|
// admin-controlled layer the endpoint is allowed to come from.
|
|
getUserSettings.mockReturnValue({
|
|
llm_provider: 'local',
|
|
llm_model: 'nuextract',
|
|
llm_base_url: 'http://ollama.internal:11434',
|
|
});
|
|
getAdminUserDefaults.mockReturnValue({ llm_provider: 'local', llm_base_url: 'http://ollama.internal:11434' });
|
|
expect(await resolver.resolve(7)).toMatchObject({ provider: 'local', baseUrl: 'http://ollama.internal:11434' });
|
|
});
|
|
|
|
it("#1772: the caller's identity does not change the answer — the resolver never reads anything keyed by it besides the settings service", async () => {
|
|
// An instance has one endpoint. An admin who parked one in their own row is
|
|
// in exactly the same position as anyone else.
|
|
getUserSettings.mockReturnValue({
|
|
llm_provider: 'local',
|
|
llm_model: 'nuextract',
|
|
llm_base_url: 'http://192.168.1.5:11434',
|
|
});
|
|
expect(await resolver.resolve(7)).toBeNull();
|
|
// The addon-row read is a single, userId-independent lookup — called once,
|
|
// with no user-scoped argument.
|
|
expect(findById).toHaveBeenCalledTimes(1);
|
|
expect(findById).toHaveBeenCalledWith('llm_parsing');
|
|
});
|
|
});
|