aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ollama-autoconnect.js
blob: b9db4ff671bcae3f277a770242a32c0be8e2ba76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// modules/test/test-ollama-autoconnect.js — run: node
// Proves the first message lazily connects instead of dying with "Use ollamaconnect first",
// and that a genuinely unreachable endpoint still produces the error (after one attempt).
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..', 'core');
const store = {};
global.localStorage = {
    getItem: k => (k in store ? store[k] : null),
    setItem: (k, v) => { store[k] = String(v); },
    removeItem: k => { delete store[k]; },
};
global.window = { addEventListener: () => {}, location: { origin: 'https://webx11.duckdns.org' } };
global.document = { addEventListener: () => {} };

const OllamaManager = new Function(
    fsMod.readFileSync(path.join(ROOT, 'ollama-manager.js'), 'utf8') + '\nreturn OllamaManager;')();

let fails = 0, passes = 0;
function ok(name, cond, detail) {
    if (cond) { passes++; return; }
    fails++;
    console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
}

function mk(behaviour) {
    const out = [];
    const term = {
        print: (m, t) => out.push({ text: String(m), type: t || '' }),
        log: () => {},
        // chat() prints the model answer through this, even in non-streaming mode
        printStreamingResponse: (t) => out.push({ text: String(t), type: 'stream' }),
    };
    const om = Object.create(OllamaManager.prototype);
    Object.assign(om, {
        terminal: term, connected: false, models: [], currentModel: 'm1',
        endpoint: 'https://webx11.duckdns.org/ollama', fallbackEndpoints: [],
        providers: [{ name: 'Ollama', type: 'ollama', endpoint: 'https://webx11.duckdns.org/ollama', model: 'm1', priority: 1, enabled: true }],
        currentProvider: null, fallbackCount: 0, responseHooks: [], _usageLog: [], _usageBuffer: [],
        _abortController: null, _connecting: null,
        saveSettings: () => {}, _saveProviders: () => {}, _logUsage: () => {}, _flushUsageLog: () => {},
    });
    om.currentProvider = om.providers[0];
    global.fetch = behaviour;
    return { om, out };
}

const json = (body, okFlag = true) => Promise.resolve({ ok: okFlag, status: okFlag ? 200 : 500, json: async () => body });

(async () => {
    // A. healthy endpoint: the first chat() call connects, then answers
    {
        const seen = [];
        const { om, out } = mk((url) => {
            seen.push(url);
            if (url.includes('/api/tags')) return json({ models: [{ name: 'm1' }] });
            // chat() goes through /api/generate for ollama providers
            return json({ response: 'pong', message: { content: 'pong' }, eval_count: 1, prompt_eval_count: 1 });
        });
        const res = await om.chat('ping');
        const text = out.map(o => o.text).join(' | ');
        ok('A1 connect is attempted lazily', seen.some(u => u.includes('/api/tags')), seen.join(', '));
        ok('A2 the user sees a connecting hint', /Connecting to Ollama/.test(text), text.slice(0, 160));
        ok('A3 manager is connected after the attempt', om.connected === true);
        ok('A4 the message is answered', String(res).includes('pong'), String(res).slice(0, 120));
        ok('A5 no "ollamaconnect first" error', !/ollamaconnect first/.test(text), text.slice(0, 200));
    }

    // B. dead endpoint: one attempt, then an actionable error
    {
        const seen = [];
        const { om, out } = mk((url) => { seen.push(url); return Promise.reject(new TypeError('Failed to fetch')); });
        const res = await om.chat('ping');
        const text = out.map(o => o.text).join(' | ');
        ok('B1 still attempts the connection', seen.some(u => u.includes('/api/tags')), seen.join(', '));
        ok('B2 returns null', res === null, String(res));
        ok('B3 error names the real cause', /endpoint is unreachable/.test(text), text.slice(0, 240));
        ok('B4 auto-connect failure is reported once', /Auto-connect failed/.test(text), text.slice(0, 240));
        ok('B5 manager stays disconnected', om.connected === false);
    }

    // C. concurrent callers share one attempt
    {
        let tagCalls = 0;
        const { om } = mk((url) => {
            if (url.includes('/api/tags')) { tagCalls++; return json({ models: [{ name: 'm1' }] }); }
            return json({ response: 'ok', message: { content: 'ok' }, eval_count: 1, prompt_eval_count: 1 });
        });
        await Promise.all([om.chat('a'), om.chat('b'), om.chat('c')]);
        ok('C1 three concurrent messages probe /api/tags once', tagCalls === 1, 'tagCalls=' + tagCalls);
    }

    if (fails === 0) {
        console.log('test-ollama-autoconnect: ' + passes + ' passed, 0 failed');
    } else {
        console.log('test-ollama-autoconnect: ' + passes + ' passed, ' + fails + ' FAILED');
        process.exit(1);
    }
})();