215 lines
7.3 KiB
TypeScript
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);
|
|
});
|
|
});
|