aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ollama-error-text.js
blob: 01a17b449b4df5bfc9c82a85693498ffeb1c483f (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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// modules/test/test-ollama-error-text.js — run: node
// Proves a failed Ollama chat names the REAL cause ("Failed to fetch", "HTTP 500")
// instead of the generic "No provider responded", and that a stale reason captured by
// an earlier, unrelated failure is never replayed on a later call.
const fsMod = require('fs'), path = require('path');
const CORE = 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(CORE, 'ollama-manager.js'), 'utf8') + '\nreturn OllamaManager;')();

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

// A manager that is already "connected" so chat() goes straight to the provider loop:
// this test is about provider failure text, not about the lazy-connect gate.
function mk() {
    const out = [];
    const term = {
        print: (m, t) => out.push({ text: String(m), type: t || '' }),
        log: () => {},
        printStreamingResponse: t => out.push({ text: String(t), type: 'stream' }),
    };
    const om = Object.create(OllamaManager.prototype);
    Object.assign(om, {
        terminal: term,
        connected: true, 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, streamEnabled: false,
        saveSettings: () => {}, _saveProviders: () => {}, _logUsage: () => {}, _flushUsageLog: () => {},
    });
    om.currentProvider = om.providers[0];
    return { om, out };
}

const json = (body, okFlag = true) => Promise.resolve({
    ok: okFlag, status: okFlag ? 200 : 500, json: async () => body,
});
const textOf = out => out.map(o => o.text).join(' | ');
const errorLines = out => out.filter(o => /Ollama error/.test(o.text)).map(o => o.text).join(' | ');

(async () => {
    // A. every provider endpoint rejects: the user must see the transport cause
    {
        const { om, out } = mk();
        global.fetch = () => Promise.reject(new TypeError('Failed to fetch'));
        const res = await om.chat('ping');
        const text = textOf(out), line = errorLines(out);
        ok('A1 chat() returns null', res === null, String(res));
        ok('A2 error line mentions "Ollama error"', /Ollama error/.test(line), 'line=' + JSON.stringify(line));
        ok('A3 error line names the real cause "Failed to fetch"', /Failed to fetch/.test(line), 'line=' + JSON.stringify(line));
        ok('A4 generic "No provider responded" is not printed', !/No provider responded/.test(text), text.slice(0, 240));
    }

    // B. provider answers HTTP 500: the status code must survive into the message
    {
        const { om, out } = mk();
        global.fetch = () => json({ error: 'kaboom' }, false);
        const res = await om.chat('ping');
        const line = errorLines(out);
        ok('B1 chat() returns null', res === null, String(res));
        ok('B2 error line carries "Ollama error"', /Ollama error/.test(line), 'line=' + JSON.stringify(line));
        ok('B3 error line carries the HTTP status "500"', /500/.test(line), 'line=' + JSON.stringify(line));
        ok('B4 generic "No provider responded" is not printed', !/No provider responded/.test(textOf(out)), textOf(out).slice(0, 240));
    }

    // C. the reason is per-call state: a later call must not replay it
    {
        const { om, out } = mk();

        // C1: a genuine failure captures a reason
        global.fetch = () => Promise.reject(new TypeError('Failed to fetch'));
        const first = await om.chat('first');

        // C2: a success in between
        global.fetch = () => json({ response: 'pong', eval_count: 1, prompt_eval_count: 1 });
        const second = await om.chat('second');
        const failMentionsAfterSuccess = out.filter(o => /Failed to fetch/.test(o.text)).length;

        // C3: a provider that fails WITHOUT throwing (empty body) — no reason to report,
        // so the fallback wording must come back instead of the stale transport error.
        global.fetch = () => json({ response: '', eval_count: 1, prompt_eval_count: 1 });
        const third = await om.chat('third');
        const lastLine = out.filter(o => /Ollama error/.test(o.text)).map(o => o.text).pop() || '';

        ok('C1 first call returns null', first === null, String(first));
        ok('C2 successful call still answers', String(second).includes('pong'), String(second).slice(0, 120));
        ok('C3 the captured reason is logged exactly once', failMentionsAfterSuccess === 1, 'count=' + failMentionsAfterSuccess);
        ok('C4 third call returns null', third === null, String(third));
        ok('C5 no-reason failure falls back to the generic wording', /No provider responded/.test(lastLine), 'last=' + JSON.stringify(lastLine));
        ok('C6 the stale "Failed to fetch" reason is not replayed', !/Failed to fetch/.test(lastLine), 'last=' + JSON.stringify(lastLine));
    }

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