feat: integrate token point usage v2
This commit is contained in:
210
tests/unit/sidebar-token-points.test.tsx
Normal file
210
tests/unit/sidebar-token-points.test.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
import type { WorksTokenPointBalance } from '@/lib/works-square';
|
||||
|
||||
const fetchTokenPointBalanceMock = vi.hoisted(() => vi.fn());
|
||||
const getValidAccessTokenMock = vi.hoisted(() => vi.fn());
|
||||
const authState = vi.hoisted(() => ({
|
||||
user: {
|
||||
username: 'member@example.com',
|
||||
userId: 'user-1',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
getValidAccessToken: getValidAccessTokenMock,
|
||||
logout: vi.fn(),
|
||||
}));
|
||||
const codingState = vi.hoisted(() => ({
|
||||
projects: [],
|
||||
activeProject: null,
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
setActiveProject: vi.fn(),
|
||||
removeProject: vi.fn(),
|
||||
createProject: vi.fn(),
|
||||
}));
|
||||
const projectConfigState = vi.hoisted(() => ({
|
||||
configsByProjectId: {},
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
remove: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (_key: string, fallback: string) => fallback }),
|
||||
}));
|
||||
vi.mock('@/lib/works-square', () => ({
|
||||
fetchWorksTokenPointBalance: (...args: unknown[]) => fetchTokenPointBalanceMock(...args),
|
||||
}));
|
||||
vi.mock('@/lib/subscription-upgrade', () => ({
|
||||
openWorksSquareSubscriptionUpgrade: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock('@/lib/api-client', () => ({ invokeIpc: vi.fn() }));
|
||||
vi.mock('@/hooks/use-current-user-profile', () => ({
|
||||
useCurrentUserProfile: () => ({
|
||||
userProfile: { displayName: '测试用户', avatarUrl: null },
|
||||
profileRequired: false,
|
||||
profileSyncError: null,
|
||||
syncProfileNow: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: (selector: (state: typeof authState) => unknown) => selector(authState),
|
||||
}));
|
||||
vi.mock('@/stores/settings', () => ({
|
||||
useSettingsStore: (selector: (state: { sidebarCollapsed: boolean }) => unknown) => (
|
||||
selector({ sidebarCollapsed: false })
|
||||
),
|
||||
}));
|
||||
vi.mock('@/stores/coding-workspace', () => ({
|
||||
useCodingWorkspaceStore: (selector: (state: typeof codingState) => unknown) => selector(codingState),
|
||||
}));
|
||||
vi.mock('@/stores/project-config', () => ({
|
||||
useProjectConfigStore: (selector: (state: typeof projectConfigState) => unknown) => (
|
||||
selector(projectConfigState)
|
||||
),
|
||||
}));
|
||||
vi.mock('@/stores/providers', () => ({
|
||||
useProviderStore: (selector: (state: { refreshProviderSnapshot: () => Promise<void> }) => unknown) => (
|
||||
selector({ refreshProviderSnapshot: vi.fn().mockResolvedValue(undefined) })
|
||||
),
|
||||
}));
|
||||
vi.mock('@/components/layout/ModuleSwitcher', () => ({ ModuleSwitcher: () => null }));
|
||||
vi.mock('@/components/layout/ImageWorkspaceSidebar', () => ({ ImageWorkspaceSidebar: () => null }));
|
||||
vi.mock('@/components/layout/LearningSidebar', () => ({ LearningSidebar: () => null }));
|
||||
vi.mock('@/components/layout/SidebarUpdateButton', () => ({ SidebarUpdateButton: () => null }));
|
||||
vi.mock('@/components/profile/UserProfileDialog', () => ({ UserProfileDialog: () => null }));
|
||||
|
||||
function pointBalance(
|
||||
overrides: Partial<WorksTokenPointBalance> = {},
|
||||
): WorksTokenPointBalance {
|
||||
return {
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '375.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '425.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function renderUsageDrawer(): Promise<HTMLElement> {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/learning']}>
|
||||
<Sidebar />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
await waitFor(() => expect(getValidAccessTokenMock).toHaveBeenCalled());
|
||||
fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger'));
|
||||
fireEvent.click(screen.getByTestId('sidebar-account-usage-menuitem'));
|
||||
return screen.getByTestId('sidebar-account-usage-drawer');
|
||||
}
|
||||
|
||||
describe('Sidebar V2 token point balance', () => {
|
||||
beforeEach(() => {
|
||||
getValidAccessTokenMock.mockResolvedValue('access-token');
|
||||
});
|
||||
|
||||
it('shows the authoritative weekly and total point balances without the retired rolling rows', async () => {
|
||||
fetchTokenPointBalanceMock.mockResolvedValue(pointBalance());
|
||||
|
||||
const drawer = await renderUsageDrawer();
|
||||
|
||||
await waitFor(() => expect(within(drawer).getByText('精通')).toBeInTheDocument());
|
||||
expect(drawer).toHaveTextContent('本周剩余');
|
||||
expect(screen.getByTestId('sidebar-weekly-token-points')).toHaveTextContent('375 / 500 点');
|
||||
expect(drawer).toHaveTextContent('总可用');
|
||||
expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('425 点');
|
||||
expect(drawer).not.toHaveTextContent('5 小时');
|
||||
expect(drawer).not.toHaveTextContent('1 周');
|
||||
});
|
||||
|
||||
it('shows a distinct retrieval error instead of treating missing data as unlimited or zero', async () => {
|
||||
fetchTokenPointBalanceMock.mockRejectedValue(new Error('upstream unavailable'));
|
||||
|
||||
const drawer = await renderUsageDrawer();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('sidebar-token-points-error')).toBeInTheDocument());
|
||||
expect(drawer).toHaveTextContent('词元点数暂时无法获取。');
|
||||
expect(drawer).not.toHaveTextContent('不限');
|
||||
expect(drawer).not.toHaveTextContent('0%');
|
||||
});
|
||||
|
||||
it('shows only coarse availability for a family-shared member', async () => {
|
||||
fetchTokenPointBalanceMock.mockResolvedValue(pointBalance({
|
||||
plan_code: null,
|
||||
plan_name: null,
|
||||
entitlement_source: 'shared_group',
|
||||
family_shared: true,
|
||||
can_manage_membership: false,
|
||||
upgrade_action: 'contact_family_owner',
|
||||
shared_available: false,
|
||||
weekly_allowance: null,
|
||||
weekly_used: null,
|
||||
weekly_reserved: null,
|
||||
weekly_remaining: null,
|
||||
permanent_total: null,
|
||||
permanent_used: null,
|
||||
permanent_reserved: null,
|
||||
permanent_remaining: null,
|
||||
total_remaining: null,
|
||||
}));
|
||||
|
||||
const drawer = await renderUsageDrawer();
|
||||
|
||||
await waitFor(() => expect(within(drawer).getByText('共享会员')).toBeInTheDocument());
|
||||
expect(drawer).toHaveTextContent('共享额度');
|
||||
expect(drawer).toHaveTextContent('已用尽');
|
||||
expect(drawer).toHaveTextContent('请联系家庭管理员');
|
||||
expect(drawer).not.toHaveTextContent('本周剩余');
|
||||
expect(drawer).not.toHaveTextContent('总可用');
|
||||
expect(screen.queryByTestId('sidebar-account-upgrade-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows only parent-managed availability for a youth self entitlement', async () => {
|
||||
fetchTokenPointBalanceMock.mockResolvedValue(pointBalance({
|
||||
plan_code: null,
|
||||
plan_name: null,
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: false,
|
||||
upgrade_action: 'contact_family_owner',
|
||||
shared_available: false,
|
||||
weekly_allowance: null,
|
||||
weekly_used: null,
|
||||
weekly_reserved: null,
|
||||
weekly_remaining: null,
|
||||
permanent_total: null,
|
||||
permanent_used: null,
|
||||
permanent_reserved: null,
|
||||
permanent_remaining: null,
|
||||
total_remaining: null,
|
||||
}));
|
||||
|
||||
const drawer = await renderUsageDrawer();
|
||||
|
||||
await waitFor(() => expect(within(drawer).getByText('青少年账户')).toBeInTheDocument());
|
||||
expect(drawer).toHaveTextContent('词元点数');
|
||||
expect(drawer).toHaveTextContent('已用尽');
|
||||
expect(drawer).toHaveTextContent('请联系家长');
|
||||
expect(drawer).not.toHaveTextContent('共享额度');
|
||||
expect(drawer).not.toHaveTextContent('本周剩余');
|
||||
expect(drawer).not.toHaveTextContent('总可用');
|
||||
expect(screen.queryByTestId('sidebar-account-upgrade-button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -592,12 +592,31 @@ describe('works square host api routes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('loads current token plan usage through the Works Square API', async () => {
|
||||
it('loads and safely projects the V2 token point balance through the Works Square API', async () => {
|
||||
const points = {
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '375.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '425.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
ignored_internal_field: 'do-not-project',
|
||||
};
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
five_hour_remaining_percent: 72.5,
|
||||
weekly_remaining_percent: 48,
|
||||
}), { status: 200 }),
|
||||
new Response(JSON.stringify(points), { status: 200 }),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
@@ -605,7 +624,7 @@ describe('works square host api routes', () => {
|
||||
const handled = await handleWorksRoutes(
|
||||
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/billing/token-usage'),
|
||||
new URL('http://127.0.0.1/api/works/billing/points'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
@@ -613,13 +632,30 @@ describe('works square host api routes', () => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
success: true,
|
||||
usage: {
|
||||
five_hour_remaining_percent: 72.5,
|
||||
weekly_remaining_percent: 48,
|
||||
points: {
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '375.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '425.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
},
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.nianxx.cn/api/billing/token-usage',
|
||||
'https://square.nianxx.cn/api/billing/points',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -629,6 +665,95 @@ describe('works square host api routes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an invalid V2 token point balance instead of forwarding it to Renderer', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
weekly_remaining: '-1.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
}), { status: 200 }),
|
||||
));
|
||||
const response = createResponse();
|
||||
|
||||
const handled = await handleWorksRoutes(
|
||||
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/billing/points'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual({
|
||||
success: false,
|
||||
error: 'Works Square returned an invalid token point balance',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes exact plan and point values from a family-shared balance', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
plan_code: 'excellence',
|
||||
plan_name: '卓越',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '1000.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '875.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '925.00',
|
||||
entitlement_source: 'shared_group',
|
||||
family_shared: true,
|
||||
can_manage_membership: false,
|
||||
upgrade_action: 'contact_family_owner',
|
||||
shared_available: true,
|
||||
}), { status: 200 }),
|
||||
));
|
||||
const response = createResponse();
|
||||
|
||||
await handleWorksRoutes(
|
||||
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/billing/points'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
success: true,
|
||||
points: {
|
||||
plan_code: null,
|
||||
plan_name: null,
|
||||
cycle_start: null,
|
||||
cycle_end: null,
|
||||
next_refresh_at: null,
|
||||
weekly_allowance: null,
|
||||
weekly_used: null,
|
||||
weekly_reserved: null,
|
||||
weekly_remaining: null,
|
||||
permanent_total: null,
|
||||
permanent_used: null,
|
||||
permanent_reserved: null,
|
||||
permanent_remaining: null,
|
||||
total_remaining: null,
|
||||
entitlement_source: 'shared_group',
|
||||
family_shared: true,
|
||||
can_manage_membership: false,
|
||||
upgrade_action: 'contact_family_owner',
|
||||
shared_available: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the current Agent Profile through the Works Square API', async () => {
|
||||
const profile = {
|
||||
display_name: '小泥',
|
||||
|
||||
63
tests/unit/works-square-token-points.test.ts
Normal file
63
tests/unit/works-square-token-points.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { WorksTokenPointBalance } from '@/lib/works-square';
|
||||
import {
|
||||
formatWorksTokenPointValue,
|
||||
isWorksTokenPointBalanceExhausted,
|
||||
} from '@/lib/works-square-token-points';
|
||||
|
||||
function pointBalance(
|
||||
overrides: Partial<WorksTokenPointBalance> = {},
|
||||
): WorksTokenPointBalance {
|
||||
return {
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '375.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '425.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Works Square V2 token points', () => {
|
||||
it('formats exact server point strings without losing integer precision', () => {
|
||||
expect(formatWorksTokenPointValue('1000.00')).toBe('1,000');
|
||||
expect(formatWorksTokenPointValue('123456789012345678.50')).toBe('123,456,789,012,345,678.5');
|
||||
expect(formatWorksTokenPointValue(null)).toBeNull();
|
||||
expect(formatWorksTokenPointValue('-1.00')).toBeNull();
|
||||
});
|
||||
|
||||
it('treats an exact zero total balance as exhausted for the owner', () => {
|
||||
expect(isWorksTokenPointBalanceExhausted(pointBalance({ total_remaining: '0.00' }))).toBe(true);
|
||||
expect(isWorksTokenPointBalanceExhausted(pointBalance())).toBe(false);
|
||||
expect(isWorksTokenPointBalanceExhausted(pointBalance({ total_remaining: null }))).toBe(false);
|
||||
});
|
||||
|
||||
it('uses the server-provided coarse availability for every non-manager', () => {
|
||||
const nonManager = pointBalance({
|
||||
plan_code: null,
|
||||
plan_name: null,
|
||||
can_manage_membership: false,
|
||||
family_shared: false,
|
||||
upgrade_action: 'contact_family_owner',
|
||||
shared_available: false,
|
||||
total_remaining: null,
|
||||
});
|
||||
expect(isWorksTokenPointBalanceExhausted(nonManager)).toBe(true);
|
||||
expect(isWorksTokenPointBalanceExhausted({ ...nonManager, shared_available: true })).toBe(false);
|
||||
expect(isWorksTokenPointBalanceExhausted({ ...nonManager, family_shared: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isWorksTokenUsageExhausted } from '@/lib/works-square-token-usage';
|
||||
|
||||
describe('isWorksTokenUsageExhausted', () => {
|
||||
it('returns true when either rolling window is exhausted', () => {
|
||||
expect(isWorksTokenUsageExhausted({
|
||||
five_hour_remaining_percent: 50,
|
||||
weekly_remaining_percent: 0,
|
||||
})).toBe(true);
|
||||
expect(isWorksTokenUsageExhausted({
|
||||
five_hour_remaining_percent: 0,
|
||||
weekly_remaining_percent: 40,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false while both rolling windows have remaining allowance', () => {
|
||||
expect(isWorksTokenUsageExhausted({
|
||||
five_hour_remaining_percent: 50,
|
||||
weekly_remaining_percent: 40,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('does not treat missing or non-finite usage as confirmed exhaustion', () => {
|
||||
expect(isWorksTokenUsageExhausted(undefined)).toBe(false);
|
||||
expect(isWorksTokenUsageExhausted({
|
||||
five_hour_remaining_percent: null,
|
||||
weekly_remaining_percent: Number.NaN,
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
fetchMyWorksProjects,
|
||||
fetchWorksAsset,
|
||||
fetchWorksAssets,
|
||||
fetchWorksTokenUsage,
|
||||
fetchWorksTokenPointBalance,
|
||||
fetchWorksProjectVersions,
|
||||
fetchWorksProjects,
|
||||
publishWorksProjectSource,
|
||||
@@ -212,22 +212,37 @@ describe('works square client', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('loads billing token usage with the current access token', async () => {
|
||||
const usage = {
|
||||
five_hour_remaining_percent: 72.5,
|
||||
weekly_remaining_percent: 48,
|
||||
plan_code: 'pro',
|
||||
plan_name: '专业版',
|
||||
it('loads the V2 token point balance with the current access token', async () => {
|
||||
const points = {
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-01T00:00:00Z',
|
||||
cycle_end: '2026-09-08T00:00:00Z',
|
||||
next_refresh_at: '2026-09-08T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: '100.00',
|
||||
weekly_reserved: '25.00',
|
||||
weekly_remaining: '375.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: '425.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
};
|
||||
hostApiFetchMock.mockResolvedValueOnce({ success: true, usage });
|
||||
hostApiFetchMock.mockResolvedValueOnce({ success: true, points });
|
||||
|
||||
const result = await fetchWorksTokenUsage('access-token');
|
||||
const result = await fetchWorksTokenPointBalance('access-token');
|
||||
|
||||
expect(result).toEqual(usage);
|
||||
expect(result.plan_code).toBe('pro');
|
||||
expect(result.plan_name).toBe('专业版');
|
||||
expect(result).toEqual(points);
|
||||
expect(result.plan_code).toBe('mastery');
|
||||
expect(result.plan_name).toBe('精通');
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith(
|
||||
'/api/works/billing/token-usage',
|
||||
'/api/works/billing/points',
|
||||
{
|
||||
headers: {
|
||||
'X-NianCode-Access-Token': 'access-token',
|
||||
|
||||
Reference in New Issue
Block a user