147 lines
5.0 KiB
JavaScript
147 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const http = require('http');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const piProvider = require('../lib/pi-provider');
|
|
|
|
function tempConfig() {
|
|
return path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'clawd-pi-')), 'models.json');
|
|
}
|
|
|
|
function apply(provider, options) {
|
|
return new Promise((resolve) => piProvider.applyFullProviderFromVps(provider, resolve, options));
|
|
}
|
|
|
|
function refresh(options) {
|
|
return new Promise((resolve) => piProvider.refreshModelsIfChanged(resolve, options));
|
|
}
|
|
|
|
function read(file) {
|
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
}
|
|
|
|
const payload = {
|
|
name: 'cutos',
|
|
'base-url': 'https://api.example.test',
|
|
'api-key': 'test-key',
|
|
};
|
|
|
|
test('creates a Pi models file and maps models to reasoning entries', async () => {
|
|
const configFile = tempConfig();
|
|
await apply(payload, {
|
|
configFile,
|
|
fetchModels: (_url, _key, done) => done(null, [
|
|
{ id: 'gpt-test', reasoning: true },
|
|
{ id: 'glm-test', reasoning: true },
|
|
]),
|
|
});
|
|
assert.deepEqual(read(configFile), {
|
|
providers: {
|
|
cutos: {
|
|
baseUrl: 'https://api.example.test/v1',
|
|
api: 'openai-responses',
|
|
apiKey: 'test-key',
|
|
models: [
|
|
{ id: 'gpt-test', reasoning: true },
|
|
{ id: 'glm-test', reasoning: true },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
test('preserves unrelated top-level fields and providers when upserting', async () => {
|
|
const configFile = tempConfig();
|
|
fs.writeFileSync(configFile, JSON.stringify({
|
|
theme: 'dark',
|
|
providers: { custom: { baseUrl: 'http://localhost:1234' } },
|
|
}));
|
|
await apply(payload, {
|
|
configFile,
|
|
fetchModels: (_url, _key, done) => done(null, [{ id: 'gpt-test', reasoning: true }]),
|
|
});
|
|
const config = read(configFile);
|
|
assert.equal(config.theme, 'dark');
|
|
assert.deepEqual(config.providers.custom, { baseUrl: 'http://localhost:1234' });
|
|
assert.equal(config.providers.cutos.api, 'openai-responses');
|
|
});
|
|
|
|
test('retains existing models when model refresh fails while updating credentials', async () => {
|
|
const configFile = tempConfig();
|
|
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {
|
|
baseUrl: 'https://old.example/v1',
|
|
api: 'openai-responses',
|
|
apiKey: 'old-key',
|
|
models: [{ id: 'existing-model', reasoning: true }],
|
|
} } }));
|
|
await apply(payload, {
|
|
configFile,
|
|
fetchModels: (_url, _key, done) => done(new Error('offline')),
|
|
});
|
|
const provider = read(configFile).providers.cutos;
|
|
assert.equal(provider.apiKey, 'test-key');
|
|
assert.deepEqual(provider.models, [{ id: 'existing-model', reasoning: true }]);
|
|
});
|
|
|
|
test('removes only the named provider on unbind', () => {
|
|
const configFile = tempConfig();
|
|
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {}, custom: { keep: true } } }));
|
|
piProvider.removeProviderByName('cutos', { configFile });
|
|
assert.deepEqual(read(configFile), { providers: { custom: { keep: true } } });
|
|
});
|
|
|
|
test('unbind cancels an in-flight provider write', async () => {
|
|
const configFile = tempConfig();
|
|
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {}, custom: { keep: true } } }));
|
|
let finishFetch;
|
|
const pending = apply(payload, {
|
|
configFile,
|
|
fetchModels: (_url, _key, done) => { finishFetch = done; },
|
|
});
|
|
piProvider.removeProviderByName('cutos', { configFile });
|
|
finishFetch(null, [{ id: 'late-model', reasoning: true }]);
|
|
await pending;
|
|
assert.deepEqual(read(configFile), { providers: { custom: { keep: true } } });
|
|
});
|
|
|
|
test('refresh updates models only when their ids change', async () => {
|
|
const configFile = tempConfig();
|
|
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {
|
|
baseUrl: 'https://api.example.test/v1', api: 'openai-responses', apiKey: 'key',
|
|
models: [{ id: 'old', reasoning: true }],
|
|
} } }));
|
|
await refresh({
|
|
configFile,
|
|
fetchModels: (_url, _key, done) => done(null, [{ id: 'new', reasoning: true }]),
|
|
});
|
|
assert.deepEqual(read(configFile).providers.cutos.models, [{ id: 'new', reasoning: true }]);
|
|
});
|
|
|
|
test('fetchModels reads OpenAI model responses without exposing the key', async () => {
|
|
const server = http.createServer((req, res) => {
|
|
assert.equal(req.url, '/v1/models');
|
|
assert.equal(req.headers.authorization, 'Bearer private-test-key');
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ data: [{ id: 'model-a' }, { id: 'model-b' }] }));
|
|
});
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
try {
|
|
const address = server.address();
|
|
const models = await new Promise((resolve, reject) => {
|
|
piProvider.fetchModels(`http://127.0.0.1:${address.port}`, 'private-test-key', (error, result) => {
|
|
if (error) reject(error); else resolve(result);
|
|
});
|
|
});
|
|
assert.deepEqual(models, [
|
|
{ id: 'model-a', reasoning: true },
|
|
{ id: 'model-b', reasoning: true },
|
|
]);
|
|
} finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
});
|