aps-agent/scripts/config.test.mjs

84 lines
3.8 KiB
JavaScript
Raw Normal View History

/** 配置加载(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');
});