Files
clawd/test/pi-provider.test.js

159 lines
5.5 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('defaults to Pi agent models config path', () => {
assert.equal(
piProvider.resolvePiModelsConfigFile({}),
path.join(os.homedir(), '.pi', 'agent', 'models.json'),
);
});
test('allows PI_MODELS_CONFIG to override Pi models config path', () => {
const customPath = path.join(os.tmpdir(), 'custom-pi-models.json');
assert.equal(piProvider.resolvePiModelsConfigFile({ PI_MODELS_CONFIG: customPath }), path.resolve(customPath));
});
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));
}
});