673 lines
23 KiB
TypeScript
673 lines
23 KiB
TypeScript
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<Record<RouteKey, unknown | (() => 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,
|
|
});
|
|
});
|
|
});
|