import test from 'node:test' import assert from 'node:assert/strict' globalThis.window = { location: { hostname: 'localhost' } } const storage = new Map() globalThis.localStorage = { getItem: key => storage.get(key) ?? null, setItem: (key, value) => storage.set(key, value), removeItem: key => storage.delete(key), } const { api } = await import('../src/lib/tauri-api.js') const channels = await import('../src/lib/model-channels.js') const devApi = await import('../scripts/dev-api.js') const originalApi = { readOpenclawConfig: api.readOpenclawConfig, writeOpenclawConfig: api.writeOpenclawConfig, revealModelChannelKey: api.revealModelChannelKey, } function restoreApi() { Object.assign(api, originalApi) storage.clear() } test.afterEach(restoreApi) test('渠道指纹覆盖模型能力元数据和凭据版本', () => { const base = { id: 'ch-1', name: '渠道', baseUrl: 'https://example.com/v1', apiType: 'openai-responses', apiKeyMask: 'sk-***same', credentialVersion: 1, models: [{ id: 'vision', input: ['text', 'image'], reasoning: true, maxTokens: 4096 }], defaultModel: 'vision', } assert.notEqual( channels.channelFingerprint(base), channels.channelFingerprint({ ...base, models: [{ ...base.models[0], maxTokens: 8192 }] }), ) assert.notEqual( channels.channelFingerprint(base), channels.channelFingerprint({ ...base, credentialVersion: 2 }), ) }) test('从 OpenClaw 导入渠道时保留完整模型能力字段', async () => { api.readOpenclawConfig = async () => ({ models: { providers: { rich: { baseUrl: 'https://example.com/v1', api: 'openai-responses', apiKey: '${RICH_API_KEY}', headers: { 'x-tenant': 'tenant-a' }, models: [{ id: 'vision', name: 'Vision', input: ['text', 'image'], reasoning: true, contextWindow: 200000, contextTokens: 160000, maxTokens: 8192, compat: { supportsDeveloperRole: false }, cost: { input: 1, output: 2 }, }], }, }, }, }) const [channel] = await channels.importChannelsFromOpenclaw([]) assert.deepEqual(channel.models[0].input, ['text', 'image']) assert.equal(channel.models[0].reasoning, true) assert.equal(channel.models[0].contextTokens, 160000) assert.equal(channel.models[0].maxTokens, 8192) assert.deepEqual(channel.models[0].compat, { supportsDeveloperRole: false }) assert.deepEqual(channel.models[0].cost, { input: 1, output: 2 }) assert.deepEqual(channel.providerConfig.headers, { 'x-tenant': 'tenant-a' }) }) test('从 OpenClaw 导入和同步渠道时原样保留结构化 SecretRef', async () => { const secretRef = { source: 'env', provider: 'default', id: 'RICH_API_KEY' } let written = null api.readOpenclawConfig = async () => written ? { models: { providers: written.models.providers } } : ({ models: { providers: { rich: { baseUrl: 'https://example.com/v1', api: 'openai-responses', apiKey: secretRef, models: [{ id: 'vision', name: 'Vision' }], }, }, }, }) api.revealModelChannelKey = async () => { throw new Error('SecretRef 同步 OpenClaw 时不应读取明文') } api.writeOpenclawConfig = async patch => { written = patch } const [channel] = await channels.importChannelsFromOpenclaw([]) assert.equal(channel.apiKey, '') assert.deepEqual(channel.apiKeyRef, secretRef) const result = await channels.syncChannelToOpenclaw(channel) assert.equal(result.verified, true) assert.deepEqual(written.models.providers.rich.apiKey, secretRef) }) test('Web 渠道存储保留 SecretRef,并允许新明文 Key 显式替换', () => { assert.equal(typeof devApi.normalizeModelChannelsDoc, 'function') const secretRef = { source: 'file', provider: 'default', id: 'providers/rich/apiKey' } const input = { channels: [{ id: 'secret-ref', name: 'Secret Ref', baseUrl: 'https://example.com/v1', apiType: 'openai-responses', apiKey: '', apiKeyRef: secretRef, models: [{ id: 'vision' }], }], } const stored = devApi.normalizeModelChannelsDoc(input, null) assert.deepEqual(stored.channels[0].apiKeyRef, secretRef) assert.equal(stored.channels[0].apiKey, '') const replaced = devApi.normalizeModelChannelsDoc({ channels: [{ ...stored.channels[0], apiKey: 'sk-new' }], }, stored) assert.equal(replaced.channels[0].apiKey, 'sk-new') assert.equal('apiKeyRef' in replaced.channels[0], false) }) test('同步旧 Codex 渠道时写入 7.1 正式 API 类型', async () => { let written = null api.revealModelChannelKey = async () => 'sk-test' api.readOpenclawConfig = async () => written || ({ models: { providers: {} } }) api.writeOpenclawConfig = async config => { written = config; return { verified: true } } await channels.syncChannelToOpenclaw({ id: 'legacy', name: 'Legacy', baseUrl: 'https://example.com/v1', apiType: 'openai-codex-responses', models: [{ id: 'gpt-test' }], defaultModel: 'gpt-test', }) assert.equal(written.models.providers.legacy.api, 'openai-chatgpt-responses') }) test('OpenClaw 同步必须通过目标配置回读核对', async () => { api.revealModelChannelKey = async () => 'sk-test' api.readOpenclawConfig = async () => ({ models: { providers: {} } }) api.writeOpenclawConfig = async () => ({ verified: true }) await assert.rejects( channels.syncChannelToOpenclaw({ id: 'verify', name: 'Verify', baseUrl: 'https://example.com/v1', apiType: 'openai-responses', models: [{ id: 'gpt-test', maxTokens: 1024 }], defaultModel: 'gpt-test', }), /回读|核对|verify/i, ) }) test('OpenClaw 同步只发送目标 provider 的最小补丁', async () => { let written = null const existing = { gateway: { port: 18789, auth: { token: 'keep-private' } }, models: { providers: { keep: { api: 'openai-completions', models: [] } } }, } api.revealModelChannelKey = async () => 'sk-target' api.readOpenclawConfig = async () => written ? { ...existing, models: { providers: { ...existing.models.providers, ...written.models.providers } } } : existing api.writeOpenclawConfig = async patch => { written = patch } await channels.syncChannelToOpenclaw({ id: 'target', name: 'Target', baseUrl: 'https://example.com/v1', apiType: 'openai-responses', models: [{ id: 'gpt-test' }], defaultModel: 'gpt-test', }) assert.deepEqual(Object.keys(written), ['models']) assert.deepEqual(Object.keys(written.models.providers), ['target']) assert.equal(written.models.providers.target.apiKey, 'sk-target') }) test('OpenClaw 同步回读必须核对凭据', async () => { api.revealModelChannelKey = async () => 'sk-expected' api.readOpenclawConfig = async () => ({ models: { providers: { target: { baseUrl: 'https://example.com/v1', api: 'openai-responses', apiKey: 'sk-wrong', models: [{ id: 'gpt-test', name: 'gpt-test' }], } } }, }) api.writeOpenclawConfig = async () => undefined await assert.rejects( channels.syncChannelToOpenclaw({ id: 'target', name: 'Target', baseUrl: 'https://example.com/v1', apiType: 'openai-responses', models: [{ id: 'gpt-test' }], defaultModel: 'gpt-test', }), /回读核对失败/, ) }) test('助手同步拒绝把环境变量引用当成 API Key 保存', () => { assert.throws( () => channels.syncChannelToAssistant({ baseUrl: 'https://example.com', apiType: 'openai-completions' }, '${OPENAI_API_KEY}'), /环境变量|env/i, ) assert.equal(storage.has(channels.ASSISTANT_STORAGE_KEY), false) }) test('助手同步仅在 localStorage 回读一致后返回 verified', () => { const result = channels.syncChannelToAssistant({ baseUrl: 'https://example.com/v1', apiType: 'openai-responses', defaultModel: 'gpt-test', }, 'sk-test', 'gpt-test') assert.equal(result.verified, true) assert.deepEqual(JSON.parse(storage.get(channels.ASSISTANT_STORAGE_KEY)), { baseUrl: 'https://example.com/v1', apiKey: 'sk-test', model: 'gpt-test', apiType: 'openai-responses', }) })