文件
mynode/packages/lib/services/ai/AiService.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

103 行
4.1 KiB
TypeScript

import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import Setting from '../../models/Setting';
import AiService from './AiService';
import deriveClassification from './classification';
import { ChatRole } from './types';
describe('AiService', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
AiService.instance().invalidateProvider();
});
it('throws when AI is disabled', async () => {
Setting.setValue('ai.enabled', false);
await expect(AiService.instance().chat([{ role: ChatRole.User, content: 'hi' }]))
.rejects.toMatchObject({ code: 'aiDisabled' });
});
it('throws when remote provider is selected but allowRemote is off', async () => {
Setting.setValue('ai.enabled', true);
Setting.setValue('ai.allowRemote', false);
Setting.setValue('ai.chat.providerType', 'anthropic');
Setting.setValue('ai.chat.apiKey', 'sk-test');
Setting.setValue('ai.chat.model', 'claude-3-5-sonnet-latest');
await expect(AiService.instance().chat([{ role: ChatRole.User, content: 'hi' }]))
.rejects.toMatchObject({ code: 'aiRemoteNotAllowed' });
});
it.each([
['http://localhost:11434/v1', 'local'],
['http://127.0.0.1:11434/v1', 'local'],
['http://[::1]:11434/v1', 'local'],
// Obfuscated loopback: new URL() resolves these to 127.0.0.1.
['http://0177.0.0.1:11434/v1', 'local'],
['http://2130706433:11434/v1', 'local'],
['http://127.1:11434/v1', 'local'],
// LAN and internal-only hosts stay within the user's private network.
['http://10.0.0.5:11434/v1', 'local'],
['http://172.16.3.9:11434/v1', 'local'],
['http://192.168.1.50:11434/v1', 'local'],
['http://169.254.1.1:11434/v1', 'local'],
['http://ollama.internal:11434/v1', 'local'],
['http://ollama.home.arpa:11434/v1', 'local'],
['http://[fd12:3456::1]:11434/v1', 'local'],
['http://[fe80::1]:11434/v1', 'local'],
// 172.15 and 172.32 are outside the 172.16/12 block.
['http://172.15.0.1:11434/v1', 'remote'],
['http://172.32.0.1:11434/v1', 'remote'],
// mDNS can resolve .local to any machine on an untrusted network.
['http://ollama.local:11434/v1', 'remote'],
// Site-local, deprecated by RFC 3879 and not treated as private.
['http://[fec0::1]:11434/v1', 'remote'],
// Leading zero makes this octal, so it resolves to the public 8.0.0.5.
['http://010.0.0.5:11434/v1', 'remote'],
['http://8.8.8.8:11434/v1', 'remote'],
['https://api.openai.com/v1', 'remote'],
['not a url', 'remote'],
['', 'remote'],
])('classifies openai-compatible %s as %s', (baseUrl, expected) => {
expect(deriveClassification('openai-compatible', baseUrl)).toBe(expected);
});
it('classifies anthropic and joplin-cloud as remote regardless of baseUrl', () => {
expect(deriveClassification('anthropic', '')).toBe('remote');
expect(deriveClassification('joplin-cloud', '')).toBe('remote');
});
it('writes joplin-cloud default on first ai.enabled if sync target is Joplin Cloud', () => {
Setting.setValue('sync.target', 10);
Setting.setValue('ai.chat.providerType.configured', false);
Setting.setValue('ai.chat.providerType', 'openai-compatible');
AiService.instance().applyFirstEnableDefault();
expect(Setting.value('ai.chat.providerType')).toBe('joplin-cloud');
expect(Setting.value('ai.chat.providerType.configured')).toBe(true);
});
it('does not touch providerType on first enable when not on Joplin Cloud', () => {
Setting.setValue('sync.target', 7);
Setting.setValue('ai.chat.providerType.configured', false);
Setting.setValue('ai.chat.providerType', 'openai-compatible');
AiService.instance().applyFirstEnableDefault();
expect(Setting.value('ai.chat.providerType')).toBe('openai-compatible');
expect(Setting.value('ai.chat.providerType.configured')).toBe(true);
});
it('does not overwrite an explicit provider choice on subsequent enable', () => {
Setting.setValue('sync.target', 10);
Setting.setValue('ai.chat.providerType.configured', true);
Setting.setValue('ai.chat.providerType', 'anthropic');
AiService.instance().applyFirstEnableDefault();
expect(Setting.value('ai.chat.providerType')).toBe('anthropic');
});
});