'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)); } });