import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { YINIAN_MODEL_AUTH_PROFILE_ID, YINIAN_MODEL_PROVIDER_KEY, YINIAN_MODEL_REF, } from '../../shared/yinian-model'; const initializerMocks = vi.hoisted(() => ({ configDir: '', runtimeDir: '', settings: {} as Record, setSetting: vi.fn(async (key: string, value: unknown) => { initializerMocks.settings[key] = value; }), ensureOfficeSkillRuntimeReady: vi.fn(async () => ({ ok: true, checks: [ { id: 'python', label: 'Python 运行环境', status: 'ok', detail: '/tmp/python' }, ], python: { executable: '/tmp/python', packages: [], }, dotnet: { available: false, version: null, }, })), })); vi.mock('@electron/utils/store', () => ({ getAllSettings: vi.fn(async () => initializerMocks.settings), setSetting: initializerMocks.setSetting, })); vi.mock('@electron/utils/paths', () => ({ getOpenClawConfigDir: () => initializerMocks.configDir, reinstallManagedOpenClawRuntime: vi.fn(() => ({ source: 'bundled', dir: initializerMocks.runtimeDir, })), })); vi.mock('@electron/utils/office-skill-runtime', () => ({ ensureOfficeSkillRuntimeReady: initializerMocks.ensureOfficeSkillRuntimeReady, })); vi.mock('@electron/utils/logger', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); function writeRuntimeFiles(runtimeDir: string): void { const files = [ 'package.json', 'openclaw.mjs', join('docs', 'reference', 'templates', 'SOUL.md'), join('docs', 'reference', 'templates', 'IDENTITY.md'), join('docs', 'reference', 'templates', 'USER.md'), join('docs', 'reference', 'templates', 'AGENTS.md'), join('docs', 'reference', 'templates', 'TOOLS.md'), join('docs', 'reference', 'templates', 'HEARTBEAT.md'), join('docs', 'reference', 'templates', 'BOOT.md'), join('node_modules', 'openclaw', 'package.json'), ]; for (const file of files) { const filePath = join(runtimeDir, file); mkdirSync(join(filePath, '..'), { recursive: true }); writeFileSync(filePath, '{}'); } } function writeInternalAuthManifest(rootDir: string, manifest: unknown): void { const manifestPath = join(rootDir, 'build', 'yinian-internal', 'model-auth-profiles.json'); mkdirSync(join(manifestPath, '..'), { recursive: true }); writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); } describe('Yinian initializer', () => { const originalCwd = process.cwd(); const originalHome = process.env.HOME; let testRoot = ''; beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); initializerMocks.settings = {}; testRoot = join(tmpdir(), `yinian-init-${Date.now()}-${Math.random().toString(16).slice(2)}`); initializerMocks.configDir = join(testRoot, '.openclaw'); initializerMocks.runtimeDir = join(initializerMocks.configDir, 'runtime', 'openclaw'); mkdirSync(testRoot, { recursive: true }); process.env.HOME = testRoot; process.chdir(testRoot); writeRuntimeFiles(initializerMocks.runtimeDir); }); afterEach(() => { process.chdir(originalCwd); process.env.HOME = originalHome; rmSync(testRoot, { recursive: true, force: true }); }); it('skips bundled model seeding for non-pilot packages and completes initialization', async () => { writeInternalAuthManifest(testRoot, { bundled: false, reason: 'YINIAN_BUNDLE_MODEL_AUTH is not enabled', }); const { initializeYinianRuntime } = await import('@electron/utils/yinian-initializer'); const result = await initializeYinianRuntime(); expect(result.initialized).toBe(true); expect(result.steps.find((step) => step.id === 'model')).toMatchObject({ status: 'success', message: '模型 API 可在设置中配置', }); expect(result.steps.find((step) => step.id === 'python')).toMatchObject({ status: 'success', }); const config = JSON.parse(readFileSync(join(initializerMocks.configDir, 'openclaw.json'), 'utf8')) as { agents?: { defaults?: { model?: unknown; workspace?: string } }; models?: { providers?: Record; mode?: string }; }; expect(config.models?.mode).toBe('merge'); expect(config.models?.providers).toEqual({}); expect(config.agents?.defaults?.model).toBeUndefined(); expect(initializerMocks.ensureOfficeSkillRuntimeReady).toHaveBeenCalled(); expect(initializerMocks.setSetting).toHaveBeenCalledWith('setupComplete', true); }); it('skips legacy bundled credentials without explicit model runtime config', async () => { writeInternalAuthManifest(testRoot, { bundled: true, store: { version: 1, profiles: { 'minimax:default': { type: 'api_key', provider: 'minimax', key: 'test-secret-key', }, }, }, }); const { initializeYinianRuntime } = await import('@electron/utils/yinian-initializer'); const result = await initializeYinianRuntime(); expect(result.initialized).toBe(true); expect(result.steps.find((step) => step.id === 'model')).toMatchObject({ status: 'success', message: '模型 API 可在设置中配置', }); const configPath = join(initializerMocks.configDir, 'openclaw.json'); const config = JSON.parse(readFileSync(configPath, 'utf8')) as { models?: { providers?: Record }; agents?: { defaults?: { model?: unknown } }; }; expect(config.models?.providers).toEqual({}); expect(config.agents?.defaults?.model).toBeUndefined(); expect(initializerMocks.ensureOfficeSkillRuntimeReady).toHaveBeenCalled(); }); it('seeds bundled pilot credentials and completes initialization', async () => { writeInternalAuthManifest(testRoot, { bundled: true, model: { providerKey: YINIAN_MODEL_PROVIDER_KEY, modelId: 'custom-model', modelName: 'Custom Model', baseUrl: 'https://api.example.com/v1', api: 'openai-completions', authProfileId: YINIAN_MODEL_AUTH_PROFILE_ID, }, store: { version: 1, profiles: { [YINIAN_MODEL_AUTH_PROFILE_ID]: { type: 'api_key', provider: YINIAN_MODEL_PROVIDER_KEY, key: 'test-secret-key', }, }, }, }); const { initializeYinianRuntime } = await import('@electron/utils/yinian-initializer'); const result = await initializeYinianRuntime(); expect(result.initialized).toBe(true); expect(result.steps.find((step) => step.id === 'model')).toMatchObject({ status: 'success', message: YINIAN_MODEL_REF, }); expect(result.steps.find((step) => step.id === 'python')).toMatchObject({ status: 'success', }); const authProfiles = JSON.parse(readFileSync( join(initializerMocks.configDir, 'agents', 'main', 'agent', 'auth-profiles.json'), 'utf8', )) as { profiles: Record }; expect(authProfiles.profiles[YINIAN_MODEL_AUTH_PROFILE_ID]).toMatchObject({ provider: YINIAN_MODEL_PROVIDER_KEY, }); expect(initializerMocks.setSetting).toHaveBeenCalledWith('setupComplete', true); }); });