Files
Genarrative/packages/shared/src/components/PlatformProfileWalletLedgerModal/index.test.tsx
T
suzmii 456cc42dd3
Project CI / Repository checks (push) Successful in 1m59s
Project CI / Frontend tests (push) Successful in 3m7s
Project CI / Backend tests (push) Successful in 6m13s
Project CI / Native shell tests (push) Successful in 19m24s
优化泥点流水的素材消耗原因展示 (#302)
Close #296

## 问题

钱包流水目前只把素材类扣费暴露为 `asset_operation_consume`,共享账单组件统一显示“资产操作消耗”,无法说明本次泥点实际用于哪类素材生成。

## 落地方案

- 在既有钱包流水 metadata 中记录服务端确定的 `assetKind`,不修改 SpacetimeDB schema。
- `api-server` 只将内部素材类型白名单映射为用户可见 `reason`,不暴露原始 metadata、资源 ID 或任务 ID。
- 共享钱包流水契约与组件优先展示具体原因;历史、未知或空 metadata 继续回退为“资产操作消耗”。
- 主站、图片画布与 AGC 继续复用同一共享账单组件。

## 当前进度

- [x] 补充权威后端数据契约
- [x] 写入素材操作类型 metadata
- [x] 扩展钱包流水公开 DTO 与安全映射
- [x] 更新共享账单展示与回归测试
- [x] 完成定向验证与边界检查

## 用户可见映射

- 图片、图标图集、美术规范、UI 设计、发布素材:生成美术素材
- 图片修改:编辑美术素材
- UI 素材提取:提取美术素材
- 角色动画:生成角色动画
- 视频:生成视频素材
- 音效:生成音效素材
- 背景音乐:生成背景音乐
- 历史、未知、空 metadata:资产操作消耗

## 验证

- `cargo fmt --all -- --check`
- `cargo test -p api-server profile_wallet_ledger_reason_only_exposes_known_asset_operations`
- `cargo test -p api-server worker_billing_context_freezes_charge_and_preserves_job_metadata`
- `cargo test -p shared-contracts profile_wallet_ledger_response_uses_camel_case_fields`
- `npx vitest run packages/shared/src/components/PlatformProfileWalletLedgerModal/index.test.tsx`
- `npm run typecheck`
- `npx eslint packages/shared/src/components/PlatformProfileWalletLedgerModal/model.ts packages/shared/src/components/PlatformProfileWalletLedgerModal/index.test.tsx packages/shared/src/contracts/runtime.ts`
- `npm run check:encoding`
- `npm run check:spacetime-schema`
- `git diff --check`

以上均通过;Rust 仅输出仓库已有 dead-code warnings。

## 未验证边界

- 未执行真实登录、生成并读取新钱包流水的端到端联调。本机 `8082/3101` 已由另一套本地栈占用,为避免当前分支挂接同一数据库并带起 worker 处理现有队列,本轮未启动 `npm run dev:api-server`。
- 四项远端 CI 已全部通过,PR 已转为 Ready for review。

## 不变项

- 不修改历史流水数据。
- 不调整泥点价格、扣费顺序、幂等 ledger、失败退款或余额结算。
- 不修改 SpacetimeDB schema。

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/302
Co-authored-by: Suzumiya <suzmii@qq.com>
Co-committed-by: Suzumiya <suzmii@qq.com>
2026-09-08 14:56:16 +08:00

176 lines
4.8 KiB
TypeScript

/* @vitest-environment jsdom */
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
import type { ProfileWalletLedgerEntry } from '../../contracts/runtime';
import { PlatformProfileWalletLedgerModal } from './index';
import {
buildWalletLedgerPresentation,
formatWalletLedgerAmount,
formatWalletLedgerDate,
getWalletLedgerSourceLabel,
} from './model';
function buildLedgerEntry(
overrides: Partial<ProfileWalletLedgerEntry> = {},
): ProfileWalletLedgerEntry {
return {
id: 'ledger-1',
amountDelta: 12,
balanceAfter: 88,
sourceType: 'daily_task_reward',
createdAt: '2026-06-10T08:00:00.000Z',
...overrides,
};
}
describe('PlatformProfileWalletLedgerModal', () => {
test('renders ledger entries with the latest balance and UTC date', () => {
render(
<PlatformProfileWalletLedgerModal
ledger={{ entries: [buildLedgerEntry()] }}
fallbackBalance={40}
isLoading={false}
error={null}
onClose={vi.fn()}
onRetry={vi.fn()}
/>,
);
const dialog = screen.getByRole('dialog', { name: '泥点账单' });
expect(within(dialog).getByText('88泥点')).toBeTruthy();
expect(within(dialog).getByText('每日任务奖励')).toBeTruthy();
expect(within(dialog).getByText('2026-06-10')).toBeTruthy();
expect(within(dialog).getByText('+12')).toBeTruthy();
expect(within(dialog).getByText('余额 88')).toBeTruthy();
});
test('supports close, loading, empty, and error retry states', async () => {
const user = userEvent.setup();
const onClose = vi.fn();
const onRetry = vi.fn();
const { rerender } = render(
<PlatformProfileWalletLedgerModal
ledger={null}
fallbackBalance={40}
isLoading
error={null}
onClose={onClose}
onRetry={onRetry}
/>,
);
expect(screen.getByRole('status', { name: '泥点账单加载中' })).toBeTruthy();
await user.click(screen.getByRole('button', { name: '关闭泥点账单' }));
expect(onClose).toHaveBeenCalledTimes(1);
rerender(
<PlatformProfileWalletLedgerModal
ledger={{ entries: [] }}
fallbackBalance={40}
isLoading={false}
error={null}
onClose={onClose}
onRetry={onRetry}
/>,
);
expect(screen.getByText('暂无账单记录')).toBeTruthy();
expect(screen.getByText('40泥点')).toBeTruthy();
rerender(
<PlatformProfileWalletLedgerModal
ledger={null}
fallbackBalance={40}
isLoading={false}
error="账单加载失败"
onClose={onClose}
onRetry={onRetry}
/>,
);
await user.click(screen.getByRole('button', { name: '重新加载' }));
expect(onRetry).toHaveBeenCalledTimes(1);
});
});
test('builds wallet ledger presentation with stable source fallbacks', () => {
expect(formatWalletLedgerAmount(-1)).toBe('-1');
expect(formatWalletLedgerAmount(0)).toBe('0');
expect(formatWalletLedgerAmount(30)).toBe('+30');
expect(getWalletLedgerSourceLabel('asset_operation_consume')).toBe(
'资产操作消耗',
);
expect(getWalletLedgerSourceLabel('future_source')).toBe('future_source');
expect(getWalletLedgerSourceLabel('llm_router_consume')).toBe('LLM 调用消耗');
expect(getWalletLedgerSourceLabel('')).toBe('未知来源');
expect(formatWalletLedgerDate('not-a-date')).toBe('not-a-date');
expect(
buildWalletLedgerPresentation(
{
entries: [
buildLedgerEntry({
amountDelta: -1,
sourceType: 'asset_operation_consume',
}),
],
},
12,
),
).toMatchObject({
balance: 88,
balanceLabel: '88泥点',
entries: [
{
amountLabel: '-1',
balanceLabel: '余额 88',
createdAtLabel: '2026-06-10',
isIncome: false,
sourceLabel: '资产操作消耗',
},
],
});
for (const reason of [
'生成美术素材',
'编辑美术素材',
'提取美术素材',
'生成角色动画',
'生成视频素材',
'生成音效素材',
'生成背景音乐',
]) {
expect(
buildWalletLedgerPresentation(
{
entries: [
buildLedgerEntry({
amountDelta: -12,
sourceType: 'asset_operation_consume',
reason,
}),
],
},
12,
).entries[0]?.sourceLabel,
`unexpected source label for ${reason}`,
).toBe(reason);
}
expect(
buildWalletLedgerPresentation(
{
entries: [
buildLedgerEntry({
amountDelta: -12,
sourceType: 'asset_operation_consume',
reason: ' ',
}),
],
},
12,
).entries[0]?.sourceLabel,
).toBe('资产操作消耗');
});