Files
NianToB/tests/unit/yinian-initializer.test.ts

215 lines
7.3 KiB
TypeScript

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<string, unknown>,
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<string, unknown>; 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<string, unknown> };
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<string, { provider?: string }> };
expect(authProfiles.profiles[YINIAN_MODEL_AUTH_PROFILE_ID]).toMatchObject({
provider: YINIAN_MODEL_PROVIDER_KEY,
});
expect(initializerMocks.setSetting).toHaveBeenCalledWith('setupComplete', true);
});
});