100 lines
4.6 KiB
JavaScript
100 lines
4.6 KiB
JavaScript
/** 配置加载(ADR-0011 / 决策 D15、D16)的用例。 */
|
||
import assert from 'node:assert/strict';
|
||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { test } from 'node:test';
|
||
|
||
import { configFilePath, isEngineConfigured, loadConfig, redactConfig } from '@aps/server/config';
|
||
|
||
async function withConfigFile(content, run) {
|
||
const dir = await mkdtemp(join(tmpdir(), 'aps-config-'));
|
||
const file = join(dir, 'config.json');
|
||
try {
|
||
if (content !== undefined) await writeFile(file, content, 'utf8');
|
||
return await run({ dir, file });
|
||
} finally {
|
||
await rm(dir, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
test('没有配置文件与环境变量时用默认值:DeepSeek,密钥为空,引擎不可用', async () => {
|
||
await withConfigFile(undefined, async ({ dir, file }) => {
|
||
const config = loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir });
|
||
assert.equal(config.llm.provider, 'deepseek');
|
||
assert.equal(config.llm.baseUrl, 'https://api.deepseek.com/v1');
|
||
assert.equal(config.llm.model, 'deepseek-chat');
|
||
assert.equal(config.llm.apiKey, '');
|
||
assert.equal(isEngineConfigured(config), false, '没有密钥时引擎不应被视为可用');
|
||
assert.equal(config.database.path, join(dir, '.aps', 'aps.sqlite'), '默认库在用户目录下');
|
||
assert.equal(config.server.port, 5199);
|
||
});
|
||
});
|
||
|
||
test('配置文件提供端点与密钥', async () => {
|
||
const content = JSON.stringify({
|
||
llm: { provider: 'compatible', baseUrl: 'http://gw.internal/v1', model: 'internal-chat', apiKey: 'sk-file-123' },
|
||
database: { path: 'D:/data/aps.sqlite' },
|
||
server: { port: 6000 },
|
||
});
|
||
await withConfigFile(content, async ({ dir, file }) => {
|
||
const config = loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir });
|
||
assert.equal(config.llm.provider, 'compatible');
|
||
assert.equal(config.llm.baseUrl, 'http://gw.internal/v1');
|
||
assert.equal(config.llm.model, 'internal-chat');
|
||
assert.equal(config.llm.apiKey, 'sk-file-123');
|
||
assert.equal(isEngineConfigured(config), true);
|
||
assert.equal(config.database.path, 'D:/data/aps.sqlite');
|
||
assert.equal(config.server.port, 6000);
|
||
});
|
||
});
|
||
|
||
test('环境变量优先于配置文件', async () => {
|
||
const content = JSON.stringify({ llm: { model: 'from-file', apiKey: 'sk-file' }, server: { port: 6000 } });
|
||
await withConfigFile(content, async ({ dir, file }) => {
|
||
const config = loadConfig({
|
||
env: { APS_CONFIG_FILE: file, APS_LLM_MODEL: 'from-env', APS_PORT: '7000' },
|
||
homeDir: dir,
|
||
});
|
||
assert.equal(config.llm.model, 'from-env');
|
||
assert.equal(config.llm.apiKey, 'sk-file', '未覆盖的字段仍取配置文件');
|
||
assert.equal(config.server.port, 7000);
|
||
});
|
||
});
|
||
|
||
test('脱敏视图不含密钥明文', async () => {
|
||
const content = JSON.stringify({ llm: { apiKey: 'sk-secret-abcdef' } });
|
||
await withConfigFile(content, async ({ dir, file }) => {
|
||
const config = loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir });
|
||
const redacted = JSON.stringify(redactConfig(config));
|
||
assert.ok(!redacted.includes('sk-secret-abcdef'), `脱敏后仍能看到密钥:${redacted}`);
|
||
assert.ok(redacted.includes('已配置'), redacted);
|
||
});
|
||
});
|
||
|
||
test('配置文件损坏时报错,不静默降级', async () => {
|
||
await withConfigFile('{ 这不是 JSON', async ({ dir, file }) => {
|
||
assert.throws(() => loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir }), /配置文件解析失败/);
|
||
});
|
||
});
|
||
|
||
test('配置文件路径:默认在用户目录,可用 APS_CONFIG_FILE 覆盖', () => {
|
||
assert.equal(configFilePath({ env: {}, homeDir: 'C:/Users/test' }).replace(/\\/g, '/'), 'C:/Users/test/.aps/config.json');
|
||
assert.equal(configFilePath({ env: { APS_CONFIG_FILE: 'D:/cfg/aps.json' }, homeDir: 'C:/Users/test' }), 'D:/cfg/aps.json');
|
||
});
|
||
|
||
test('引擎选择:没密钥用假引擎,有密钥自动选 Pi,可显式覆盖', async () => {
|
||
await withConfigFile(undefined, async ({ dir, file }) => {
|
||
const noKey = loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir });
|
||
assert.equal(noKey.engine.kind, 'fake', '没有密钥时应回退到假引擎');
|
||
});
|
||
|
||
const content = JSON.stringify({ llm: { apiKey: 'sk-file-123' } });
|
||
await withConfigFile(content, async ({ dir, file }) => {
|
||
const withKey = loadConfig({ env: { APS_CONFIG_FILE: file }, homeDir: dir });
|
||
assert.equal(withKey.engine.kind, 'pi', '有密钥时应自动选 Pi');
|
||
|
||
const forced = loadConfig({ env: { APS_CONFIG_FILE: file, APS_ENGINE: 'fake' }, homeDir: dir });
|
||
assert.equal(forced.engine.kind, 'fake', '显式配置应覆盖自动选择');
|
||
});
|
||
}); |