import { afterEach, describe, expect, it, vi } from 'vitest'; import { MockYinianControlPlane } from '@electron/yinian/mock-control-plane'; import { createMemoryYinianStorage } from '@electron/yinian/storage'; import { HttpYinianControlPlane } from '@electron/yinian/http-control-plane'; import { getYinianControlPlane, resetYinianControlPlaneForTests } from '@electron/yinian/control-plane'; import { yinianContractFixtures } from '../fixtures/yinian-server-contract'; type RouteKey = `${string} ${string}`; function jsonResponse(body: unknown, init: ResponseInit = {}): Response { return new Response(JSON.stringify(body), { status: init.status ?? 200, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), }, }); } function installFetchRoutes(routes: Partial unknown)>>) { const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { const requestUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; const url = new URL(requestUrl); const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); const key = `${method} ${url.pathname}` as RouteKey; const route = routes[key]; if (!route) { return jsonResponse({ error: { code: 'NOT_FOUND', message: `Unexpected request: ${key}`, }, }, { status: 404 }); } return jsonResponse(typeof route === 'function' ? route() : route); }); vi.stubGlobal('fetch', fetchMock); return fetchMock; } afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); resetYinianControlPlaneForTests(); }); describe('MockYinianControlPlane', () => { it('persists saved login credentials in the YINIAN storage namespace', async () => { const storage = createMemoryYinianStorage(); await storage.setSavedCredentials({ account: '15685275886-1', password: '123456', rememberPassword: true, updatedAt: 1, }); await expect(storage.getSavedCredentials()).resolves.toEqual({ account: '15685275886-1', password: '123456', rememberPassword: true, updatedAt: 1, }); await storage.clearSavedCredentials(); await expect(storage.getSavedCredentials()).resolves.toBeUndefined(); }); it('starts unauthenticated and logs in with password', async () => { const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); await expect(controlPlane.getSessionState()).resolves.toEqual({ authenticated: false }); const session = await controlPlane.loginWithPassword({ account: 'admin', password: 'demo-password', }); expect(session.authenticated).toBe(true); expect(session.user.name).toBe('NIANXX 实施'); expect(session.hotels).toHaveLength(2); }); it('switches workspace and returns matching config snapshot', async () => { const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); const switched = await controlPlane.switchHotel('workspace_shanghai_growth'); expect(switched.currentHotelId).toBe('workspace_shanghai_growth'); const config = await controlPlane.getConfigSnapshot(); expect(config.hotel.name).toBe('智念增长组织空间'); expect(config.entitlements.map((skill) => skill.skillId)).toContain('daily-report'); }); it('syncs entitled skills into local registry', async () => { const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); const firstSync = await controlPlane.syncSkills(); expect(firstSync.skills).toHaveLength(4); expect(firstSync.skills.every((skill) => skill.status === 'installed')).toBe(true); const secondSync = await controlPlane.syncSkills(); expect(secondSync.skills.every((skill) => skill.status === 'skipped')).toBe(true); const localSkills = await controlPlane.listLocalSkills(); expect(localSkills.map((skill) => skill.skillId)).toContain('data-check'); }); it('restores mock session and registry from storage', async () => { const storage = createMemoryYinianStorage(); const firstControlPlane = new MockYinianControlPlane({ storage }); await firstControlPlane.loginWithPassword({ account: 'admin', password: 'demo-password' }); await firstControlPlane.syncSkills(); const restoredControlPlane = new MockYinianControlPlane({ storage }); const session = await restoredControlPlane.restoreSession(); expect(session.authenticated).toBe(true); if (!session.authenticated) return; expect(session.currentHotelId).toBe('workspace_hangzhou_ops'); const localSkills = await restoredControlPlane.listLocalSkills(); expect(localSkills).toHaveLength(4); }); it('clears session and current workspace skill registry on logout', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new MockYinianControlPlane({ storage }); await controlPlane.loginWithSms({ phone: '13800000000', code: '123456' }); await controlPlane.syncSkills(); await controlPlane.logout(); await expect(controlPlane.restoreSession()).resolves.toEqual({ authenticated: false }); expect(await controlPlane.getSkillRegistry('workspace_hangzhou_ops')).toBeUndefined(); }); it('rejects config access before login', async () => { const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); await expect(controlPlane.getConfigSnapshot()).rejects.toThrow('请先登录'); }); }); describe('HttpYinianControlPlane', () => { it('logs in and normalizes contract v0 config responses', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test/', storage, }); const fetchMock = installFetchRoutes({ 'POST /auth/oauth2/token': yinianContractFixtures.login, 'GET /config/sync': yinianContractFixtures.config, }); const session = await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret', captchaCode: '5678nianxx', randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', }); expect(session.authenticated).toBe(true); expect(session.user.name).toBe('王管理员'); expect(session.currentHotelId).toBe('workspace_hangzhou_ops'); expect(session.accessTokenExpiresAt).toBe(1777188600000); const persisted = await storage.getSession(); expect(persisted?.mode).toBe('http'); expect(persisted?.refreshToken).toBe('refresh_demo'); const config = await controlPlane.getConfigSnapshot(); expect(config.serverTime).toBe(1777188000000); expect(config.hotel.id).toBe('workspace_hangzhou_ops'); expect(config.featureFlags).toEqual({ skillsSync: true, advancedSettings: false, }); expect(config.uiPolicy).toEqual({ defaultPage: 'today', showAdvancedSettings: true, }); expect(config.entitlements[0]).toMatchObject({ skillId: 'daily-report', category: 'reporting', triggers: ['scheduled', 'manual'], lastRunAt: 1777184400000, }); expect(config.entitlements[1]).toMatchObject({ skillId: 'unknown-category-skill', category: 'ops-automation', }); expect(config.notificationChannels.map((channel) => channel.source)).toEqual(['nianxx', 'kernel']); expect(fetchMock).not.toHaveBeenCalledWith('https://api.example.test/auth/me', expect.anything()); expect(fetchMock).toHaveBeenCalledWith( 'https://api.example.test/auth/oauth2/token', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/x-www-form-urlencoded', Authorization: 'Basic Y3VzdG9tUEM6Y3VzdG9tUEM=', }), body: expect.stringContaining('grant_type=password'), }), ); const tokenCall = fetchMock.mock.calls.find(([url]) => url === 'https://api.example.test/auth/oauth2/token'); const tokenBody = new URLSearchParams(String(tokenCall?.[1]?.body ?? '')); expect(Object.fromEntries(tokenBody)).toMatchObject({ grant_type: 'password', scope: 'server', username: 'ops@example.com', password: 'secret', randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', code: '5678nianxx', }); expect(tokenBody.has('clientId')).toBe(false); expect(tokenBody.has('client_id')).toBe(false); }); it('syncs manifest into workspace registry and marks same-version sync as skipped', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); installFetchRoutes({ 'POST /auth/oauth2/token': yinianContractFixtures.login, 'GET /auth/me': yinianContractFixtures.me, 'GET /skills/manifest': yinianContractFixtures.manifest, }); await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); const firstSync = await controlPlane.syncSkills(); expect(firstSync.skills.map((skill) => skill.status)).toEqual(['installed', 'disabled']); expect(firstSync.skills[0]).toMatchObject({ skillId: 'data-check', bundleSha256: 'sha256-demo', source: 'nianxx', }); const secondSync = await controlPlane.syncSkills(); expect(secondSync.skills.map((skill) => skill.status)).toEqual(['skipped', 'disabled']); const registry = await controlPlane.getSkillRegistry('workspace_hangzhou_ops'); expect(registry && 'skills' in registry ? registry.skills : []).toHaveLength(2); }); it('supports configurable enterprise app manifest endpoints and app-shaped fields', async () => { vi.stubEnv('YINIAN_SKILLS_MANIFEST_PATH', '/enterprise/spaces/{workspaceId}/apps?includeBundle=true'); const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); const fetchMock = installFetchRoutes({ 'POST /auth/oauth2/token': yinianContractFixtures.login, 'GET /enterprise/spaces/workspace_hangzhou_ops/apps': { data: { records: [ { appId: 'daily-report', appName: '日报生成助手', currentVersion: '2.1.0', status: 'enabled', bundleHash: 'sha256-cloudclaw-demo', }, { app_id: 'disabled-helper', app_name: '停用应用', app_version: '1.0.0', status: 'disabled', }, ], }, }, }); await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); const result = await controlPlane.syncSkills(); expect(fetchMock).toHaveBeenCalledWith( 'https://api.example.test/enterprise/spaces/workspace_hangzhou_ops/apps?includeBundle=true', expect.anything(), ); expect(result.skills).toEqual([ expect.objectContaining({ skillId: 'daily-report', name: '日报生成助手', version: '2.1.0', status: 'installed', bundleSha256: 'sha256-cloudclaw-demo', }), expect.objectContaining({ skillId: 'disabled-helper', name: '停用应用', status: 'disabled', }), ]); }); it('stores an empty registry when the enterprise app manifest endpoint is not implemented yet', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request, init?: RequestInit) => { const requestUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; const url = new URL(requestUrl); const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); if (method === 'POST' && url.pathname === '/auth/oauth2/token') { return jsonResponse(yinianContractFixtures.login); } if (method === 'GET' && url.pathname === '/skills/manifest') { return jsonResponse({ message: 'No static resource skills/manifest.' }, { status: 404 }); } return jsonResponse({ message: `Unexpected request: ${method} ${url.pathname}` }, { status: 404 }); })); await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); const result = await controlPlane.syncSkills(); expect(result.skills).toEqual([]); const registry = await controlPlane.getSkillRegistry('workspace_hangzhou_ops'); expect(registry && 'skills' in registry ? registry.skills : []).toEqual([]); }); it('restores an HTTP session through refresh token exchange', async () => { const storage = createMemoryYinianStorage(); await storage.setSession({ mode: 'http', user: { id: 'old_user', name: '旧用户' }, hotels: [], currentHotelId: 'workspace_hangzhou_ops', accessTokenExpiresAt: 1, refreshToken: 'refresh_demo', updatedAt: 1, }); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); installFetchRoutes({ 'POST /auth/oauth2/token': yinianContractFixtures.refresh, }); const session = await controlPlane.restoreSession(); expect(session.authenticated).toBe(true); if (!session.authenticated) return; expect(session.user.id).toBe('user_ops_001'); expect((await storage.getSession())?.refreshToken).toBe('refresh_rotated'); }); it('clears persisted HTTP session when refresh fails', async () => { const storage = createMemoryYinianStorage(); await storage.setSession({ mode: 'http', user: { id: 'old_user', name: '旧用户' }, hotels: [], currentHotelId: 'workspace_hangzhou_ops', accessTokenExpiresAt: 1, refreshToken: 'expired_refresh', updatedAt: 1, }); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(yinianContractFixtures.error, { status: 401 }))); await expect(controlPlane.restoreSession()).resolves.toEqual({ authenticated: false }); await expect(storage.getSession()).resolves.toBeUndefined(); }); it('surfaces server error messages', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage: createMemoryYinianStorage(), }); vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(yinianContractFixtures.error, { status: 401 }))); await expect(controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'bad-secret', })).rejects.toThrow('Session expired'); }); it('turns network failures into readable connection errors', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://onefeel.brother7.cn/ingress', storage: createMemoryYinianStorage(), }); vi.stubGlobal('fetch', vi.fn(async () => { throw new TypeError('fetch failed', { cause: Object.assign(new Error('getaddrinfo ENOTFOUND onefeel.brother7.cn'), { code: 'ENOTFOUND', syscall: 'getaddrinfo', hostname: 'onefeel.brother7.cn', }), }); })); await expect(controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret', })).rejects.toThrow('域名 onefeel.brother7.cn 解析失败'); }); it('does not send SMS login requests in HTTP mode', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage: createMemoryYinianStorage(), }); const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); await expect(controlPlane.loginWithSms({ phone: '13800000000', code: '123456' })) .rejects.toThrow('仅支持账号密码登录'); expect(fetchMock).not.toHaveBeenCalled(); }); it('accepts data-wrapped login responses with session payload', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); const fetchMock = installFetchRoutes({ 'POST /auth/oauth2/token': { success: true, data: { access_token: 'access_wrapped', refresh_token: 'refresh_wrapped', expires_in: 3600, user: yinianContractFixtures.me.user, workspaces: yinianContractFixtures.me.hotels, current_workspace_id: 'workspace_shanghai_growth', }, }, }); const session = await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret', }); expect(session.authenticated).toBe(true); expect(session.currentHotelId).toBe('workspace_shanghai_growth'); expect(session.hotels).toHaveLength(2); expect((await storage.getSession())?.refreshToken).toBe('refresh_wrapped'); expect(fetchMock).toHaveBeenCalledTimes(1); }); it('accepts oauth token responses with user_info and no workspace list', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); installFetchRoutes({ 'POST /auth/oauth2/token': { access_token: 'access_onefeel', refresh_token: 'refresh_onefeel', expires_in: '9999', username: '15685275886-1', user_info: { id: '1965411380338970626', username: '15685275886-1', tenantId: '1', name: '15685275886-1', }, }, }); const session = await controlPlane.loginWithPassword({ account: '15685275886-1', password: '123456', captchaCode: '5678nianxx', randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', }); expect(session.authenticated).toBe(true); expect(session.user.id).toBe('1965411380338970626'); expect(session.hotels).toEqual([ expect.objectContaining({ id: 'service_1', name: '15685275886-1的组织空间', }), ]); expect(session.currentHotelId).toBe('service_1'); expect(session.accessTokenExpiresAt).toBeGreaterThan(Date.now()); expect((await storage.getSession())?.refreshToken).toBe('refresh_onefeel'); }); it('creates a local organization session when oauth token response has no profile payload', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); installFetchRoutes({ 'POST /auth/oauth2/token': { access_token: 'access_without_profile', refresh_token: 'refresh_without_profile', expires_in: 3600, }, }); const session = await controlPlane.loginWithPassword({ account: 'tmt1', password: '123456', }); expect(session.authenticated).toBe(true); expect(session.user).toMatchObject({ id: 'tmt1', name: 'tmt1', }); expect(session.hotels).toEqual([ expect.objectContaining({ id: 'service_default', name: 'tmt1的组织空间', }), ]); expect(session.currentHotelId).toBe('service_default'); expect((await storage.getSession())?.refreshToken).toBe('refresh_without_profile'); }); it('uses JWT claims as a fallback profile for oauth token responses', async () => { const storage = createMemoryYinianStorage(); const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage, }); const claims = Buffer.from(JSON.stringify({ user_id: 'jwt-user-1', user_name: 'jwt-user', tenant_id: 'tenant-9', tenant_name: 'JWT 组织', })).toString('base64url'); installFetchRoutes({ 'POST /auth/oauth2/token': { access_token: `header.${claims}.signature`, refresh_token: 'refresh_jwt', expires_in: 3600, }, }); const session = await controlPlane.loginWithPassword({ account: 'fallback-account', password: '123456', }); expect(session.user).toMatchObject({ id: 'jwt-user-1', name: 'jwt-user', }); expect(session.hotels).toEqual([ expect.objectContaining({ id: 'service_tenant-9', name: 'JWT 组织', }), ]); }); it('reports HTTP server status through health endpoint', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage: createMemoryYinianStorage(), }); installFetchRoutes({ 'GET /health': { data: { server_time: 1777188000000, version: '2026.04.26', message: 'ok', }, }, }); await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ mode: 'http', apiBaseUrl: 'https://api.example.test', reachable: true, serverTime: 1777188000000, version: '2026.04.26', message: 'ok', }); }); it('loads image captcha with randomStr', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage: createMemoryYinianStorage(), }); installFetchRoutes({ 'GET /auth/code/image': { data: { randomStr: 'captcha-key', imageBase64: 'aW1hZ2U=', mimeType: 'image/png', }, }, }); await expect(controlPlane.createImageCaptcha('captcha-key')).resolves.toMatchObject({ randomStr: 'captcha-key', imageBase64: 'aW1hZ2U=', mimeType: 'image/png', }); }); it('loads binary image captcha responses', async () => { const controlPlane = new HttpYinianControlPlane({ apiBaseUrl: 'https://api.example.test', storage: createMemoryYinianStorage(), }); vi.stubGlobal('fetch', vi.fn(async () => new Response(new Uint8Array([137, 80, 78, 71]), { status: 200, headers: { 'Content-Type': 'image/png', }, }))); await expect(controlPlane.createImageCaptcha('captcha-key')).resolves.toMatchObject({ randomStr: 'captcha-key', imageBase64: 'iVBORw==', mimeType: 'image/png', }); }); }); describe('getYinianControlPlane', () => { it('uses the configured default server instead of mock mode when env URL is missing', async () => { vi.stubEnv('YINIAN_API_BASE_URL', ''); vi.stubEnv('YINIAN_CONTROL_PLANE_MODE', ''); vi.stubEnv('CLAWX_E2E', ''); installFetchRoutes({ 'GET /ingress/health': { serverTime: 1700000000000, version: 'test' }, }); const controlPlane = getYinianControlPlane(); await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ mode: 'http', apiBaseUrl: 'https://onefeel.brother7.cn/ingress', reachable: true, }); }); it('uses mock mode only when explicitly requested', async () => { vi.stubEnv('YINIAN_API_BASE_URL', ''); vi.stubEnv('YINIAN_CONTROL_PLANE_MODE', 'mock'); vi.stubEnv('CLAWX_E2E', ''); const controlPlane = getYinianControlPlane(); await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ mode: 'mock', reachable: true, }); }); });