456cc42dd3
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>
176 lines
4.8 KiB
TypeScript
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('资产操作消耗');
|
|
});
|