feat(agc): 面板按衍生作品采集核心改动说明、只读继承授权并登记详情命令
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -144,6 +144,10 @@ const allowedUncalledTauriCommands = [
|
||||
// 同一批的 `list_platform_game_catalog` 与 `create_local_project_from_platform_game`
|
||||
// 都是字面量调用点,不需要登记。
|
||||
'read_public_game_cover_preview',
|
||||
// 父作品卡(Fork 工程确认页与发布面板):与封面同理,命令名以常量 `DETAIL_COMMAND` 传给
|
||||
// **注入的** invoke(`src/features/platform-fork/forkParentCard.ts`),静态扫描只认字面量
|
||||
// 调用点,因此显式登记为 native-only 白名单。
|
||||
'read_public_game_detail',
|
||||
// 账户与钱包由 Rust typed command 持有 origin/Bearer/envelope;命令名在 `accountHost.ts`
|
||||
// 里以字面量出现,静态扫描仍按共享注册表核验。
|
||||
'read_profile_recharge_center',
|
||||
|
||||
+163
-33
@@ -17,6 +17,7 @@ import {
|
||||
import { IMAGE_MODEL_GPT_IMAGE_2 } from '../../../../../src/components/image-editor/ImageCanvasGenerationModel';
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type { LocalProjectExportPackageResult } from '../../app/types';
|
||||
import { loadForkParentCard } from '../../features/platform-fork/forkParentCard';
|
||||
import { uploadPlatformMediaAsset } from '../../services/assetDirectUpload';
|
||||
import {
|
||||
type GameDistributionPublicationImageDraft,
|
||||
@@ -37,6 +38,12 @@ import {
|
||||
} from '../../services/gamePackageUploadProgress';
|
||||
import type { TauriEventUnsubscribe } from '../../services/tauriEventSubscription';
|
||||
import { ThemedModal } from '../modal/ThemedModal';
|
||||
import {
|
||||
CHANGE_SUMMARY_MAX_CHARS,
|
||||
CHANGE_SUMMARY_MIN_CHARS,
|
||||
describePublishFailure,
|
||||
validateChangeSummary,
|
||||
} from './changeSummary';
|
||||
import { GamePublishPhaseSteps } from './GamePublishPhaseSteps';
|
||||
|
||||
/** 封面与截图都是公开展示素材,限制单张体积,避免手机原图直传拖垮发布流程。 */
|
||||
@@ -264,6 +271,16 @@ export function GameDistributionPublishPanel({
|
||||
* 只是退化为「只能被产物级改编」。
|
||||
*/
|
||||
const [projectBundleConfirmed, setProjectBundleConfirmed] = useState(true);
|
||||
/**
|
||||
* 本项目的改编来源记录(衍生作品才有)。
|
||||
*
|
||||
* 衍生作品的差别是服务端口径:**「本次核心改动说明」必填**、**授权档位继承父作品且不可选**。
|
||||
*/
|
||||
const [forkSource, setForkSource] = useState<{ gameId: string } | null>(null);
|
||||
/** 父作品的授权档位文案(只读展示用);取不到就只显示「继承自父作品」。 */
|
||||
const [parentAuthorizationLabel, setParentAuthorizationLabel] = useState('');
|
||||
const [changeSummary, setChangeSummary] = useState('');
|
||||
const [changeSummaryError, setChangeSummaryError] = useState('');
|
||||
/** 付费方式与买断价输入;校验口径与网页、服务端一致。 */
|
||||
const [priceMode, setPriceMode] = useState<PlatformGamePricingMode>('free');
|
||||
const [priceInput, setPriceInput] = useState('');
|
||||
@@ -493,6 +510,55 @@ export function GameDistributionPublishPanel({
|
||||
}
|
||||
}, [manifest, open, packageResult?.packageRelativePath, projectPath]);
|
||||
|
||||
/**
|
||||
* 衍生作品的判定与父作品档位:读项目里的 `.agent/fork-source.json`。
|
||||
*
|
||||
* 服务端口径:衍生作品的「本次核心改动说明」必填、授权档位继承父作品且不可选;母版反之。
|
||||
* 记录缺失/损坏按「母版」处理(与发布链路读它的口径一致),父作品档位取不到就只显示
|
||||
* 「继承自父作品」——不猜档位。
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!open || !projectPath.trim()) {
|
||||
setForkSource(null);
|
||||
setParentAuthorizationLabel('');
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setForkSource(null);
|
||||
setParentAuthorizationLabel('');
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void invoke<{ gameId?: string } | null>('read_local_project_fork_source', {
|
||||
projectPath,
|
||||
})
|
||||
.then(async (record) => {
|
||||
if (cancelled) return;
|
||||
const parentGameId =
|
||||
typeof record?.gameId === 'string' ? record.gameId.trim() : '';
|
||||
setForkSource(parentGameId ? { gameId: parentGameId } : null);
|
||||
if (!parentGameId) {
|
||||
setParentAuthorizationLabel('');
|
||||
return;
|
||||
}
|
||||
const card = await loadForkParentCard(parentGameId);
|
||||
if (!cancelled) {
|
||||
setParentAuthorizationLabel(card?.authorizationLabel ?? '');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 读来源记录失败不阻塞发布:按母版处理,必填校验与授权判据仍由服务端兜底。
|
||||
if (!cancelled) {
|
||||
setForkSource(null);
|
||||
setParentAuthorizationLabel('');
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, projectPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
@@ -749,6 +815,16 @@ export function GameDistributionPublishPanel({
|
||||
setError('素材还在上传中,请稍候再发布');
|
||||
return;
|
||||
}
|
||||
// 衍生作品的「本次核心改动说明」必填(母版可空):本地先拦一次给可读提示,
|
||||
// 服务端仍是唯一判据(超限/缺失都会带稳定错误码回来)。
|
||||
if (forkSource) {
|
||||
const summaryError = validateChangeSummary(changeSummary);
|
||||
if (summaryError) {
|
||||
setChangeSummaryError(summaryError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setChangeSummaryError('');
|
||||
const price = resolvePlatformGamePriceMudPoints(priceMode, priceInput);
|
||||
if (price.error) {
|
||||
setPriceError(price.error);
|
||||
@@ -824,13 +900,15 @@ export function GameDistributionPublishPanel({
|
||||
...(forkAuthorization !== 'forbidden' && !projectBundleConfirmed
|
||||
? { includeProjectBundle: false }
|
||||
: {}),
|
||||
// 衍生作品必填的「本次核心改动说明」;母版不传(服务端对母版忽略该字段)。
|
||||
...(forkSource && changeSummary.trim()
|
||||
? { changeSummary: changeSummary.trim() }
|
||||
: {}),
|
||||
});
|
||||
setResult(next);
|
||||
onPublished?.(next);
|
||||
} catch (nextError) {
|
||||
setError(
|
||||
nextError instanceof Error ? nextError.message : String(nextError),
|
||||
);
|
||||
setError(describePublishFailure(nextError));
|
||||
} finally {
|
||||
unsubscribe();
|
||||
setBusy(false);
|
||||
@@ -983,6 +1061,34 @@ export function GameDistributionPublishPanel({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{/*
|
||||
衍生作品必填的「本次核心改动说明」:版本级事实,母版不需要(服务端对母版忽略它)。
|
||||
长度上限与服务端同值,按字符计;本地只拦「能不能提交」,判据仍在服务端。
|
||||
*/}
|
||||
{forkSource ? (
|
||||
<label>
|
||||
本次核心改动说明({CHANGE_SUMMARY_MIN_CHARS}–
|
||||
{CHANGE_SUMMARY_MAX_CHARS} 字,必填)
|
||||
<textarea
|
||||
aria-label="本次核心改动说明"
|
||||
maxLength={CHANGE_SUMMARY_MAX_CHARS}
|
||||
rows={3}
|
||||
value={changeSummary}
|
||||
onChange={(event) => {
|
||||
setChangeSummary(event.target.value);
|
||||
if (changeSummaryError) {
|
||||
setChangeSummaryError('');
|
||||
}
|
||||
}}
|
||||
placeholder="这一版相对父作品改了什么(用于族谱溯源里的差异说明)"
|
||||
disabled={busy || publicationState === 'checking'}
|
||||
/>
|
||||
<small className="game-distribution-publish-panel__fork-hint">
|
||||
已填 {Array.from(changeSummary.trim()).length} 字
|
||||
{changeSummaryError ? ` · ${changeSummaryError}` : ''}
|
||||
</small>
|
||||
</label>
|
||||
) : null}
|
||||
{/*
|
||||
授权共创三态单选。与分类一样是「label + 受控控件」单元,但三态必须都能看见,
|
||||
因此用 radio 组而不是 select;文案直接复用平台契约里的标签常量,不另造词。
|
||||
@@ -994,43 +1100,67 @@ export function GameDistributionPublishPanel({
|
||||
}
|
||||
>
|
||||
<legend>授权共创</legend>
|
||||
<div
|
||||
className="game-distribution-publish-panel__fork-options"
|
||||
role="radiogroup"
|
||||
aria-label="授权共创"
|
||||
>
|
||||
{GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.map((value) => (
|
||||
<label key={value}>
|
||||
<input
|
||||
type="radio"
|
||||
name="game-distribution-fork-authorization"
|
||||
value={value}
|
||||
checked={forkAuthorization === value}
|
||||
disabled={forkAuthorizationLocked}
|
||||
onChange={() => {
|
||||
setForkAuthorization(value);
|
||||
// 每次选择授权档位都回到「默认已勾选」:避免把上一次的取消状态带到
|
||||
// 新的档位选择上(禁止共创时不显示也不上传,值本身无意义)。
|
||||
setProjectBundleConfirmed(value !== 'forbidden');
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[value]}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{/*
|
||||
衍生作品的授权由服务端**继承父作品**、且之后禁止再提升,因此面板不提供选择:
|
||||
这里只读展示父作品档位(取不到就只说「继承自父作品」,不猜档位)。
|
||||
母版才给三态单选。
|
||||
*/}
|
||||
{forkSource ? (
|
||||
<p
|
||||
className="game-distribution-publish-panel__fork-hint"
|
||||
aria-label="授权继承"
|
||||
>
|
||||
{`授权继承自父作品${
|
||||
parentAuthorizationLabel
|
||||
? `:${parentAuthorizationLabel}`
|
||||
: '(档位读取中或不可用)'
|
||||
}。衍生作品的授权档位在创建时由父作品快照继承,本页与网页端都不能再修改(服务端会拒绝任何提升)。`}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
className="game-distribution-publish-panel__fork-options"
|
||||
role="radiogroup"
|
||||
aria-label="授权共创"
|
||||
>
|
||||
{GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.map((value) => (
|
||||
<label key={value}>
|
||||
<input
|
||||
type="radio"
|
||||
name="game-distribution-fork-authorization"
|
||||
value={value}
|
||||
checked={forkAuthorization === value}
|
||||
disabled={forkAuthorizationLocked}
|
||||
onChange={() => {
|
||||
setForkAuthorization(value);
|
||||
// 每次选择授权档位都回到「默认已勾选」:避免把上一次的取消状态带到
|
||||
// 新的档位选择上(禁止共创时不显示也不上传,值本身无意义)。
|
||||
setProjectBundleConfirmed(value !== 'forbidden');
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[value]}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="game-distribution-publish-panel__fork-hint">
|
||||
{forkAuthorizationLocked
|
||||
? '共创授权只在首次发布时生效;上架后只能在网页端「我的作品」单向提升。'
|
||||
: '授权非「禁止共创」即允许他人 Fork 你的作品;开启共创并上传工程时,整个工程(含素材与文档)会对他人可见,且不可撤销。'}
|
||||
{forkSource
|
||||
? '授权非「禁止共创」即允许他人 Fork 你的作品;开启共创并上传工程时,整个工程(含素材与文档)会对他人可见,且不可撤销。'
|
||||
: forkAuthorizationLocked
|
||||
? '共创授权只在首次发布时生效;上架后只能在网页端「我的作品」单向提升。'
|
||||
: '授权非「禁止共创」即允许他人 Fork 你的作品;开启共创并上传工程时,整个工程(含素材与文档)会对他人可见,且不可撤销。'}
|
||||
</p>
|
||||
{/*
|
||||
工程源码外发是**知情同意**:工程源包的拒绝规则是黑名单,作者的 docs/、截图、
|
||||
设计稿、笔记等不在任何拒绝集内,会随包一起对任何登录用户公开。默认勾选与授权
|
||||
语义一致(允许共创即开放源码),但作者可以取消:取消即本次不上传源码。
|
||||
衍生作品没有本地档位可选,按父作品档位判断(读不到就按「非禁止」展示——
|
||||
既然能 Fork 出来,父作品档位必然不是禁止)。
|
||||
*/}
|
||||
{forkAuthorization !== 'forbidden' &&
|
||||
{(forkSource
|
||||
? parentAuthorizationLabel !== '禁止共创'
|
||||
: forkAuthorization !== 'forbidden') &&
|
||||
!forkAuthorizationLocked ? (
|
||||
<label className="game-distribution-publish-panel__bundle-consent">
|
||||
<input
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 衍生作品「本次核心改动说明」的本地拦挡与错误文案。
|
||||
*
|
||||
* 长度区间与服务端 `GAME_DISTRIBUTION_CHANGE_SUMMARY_MIN_CHARS` / `_MAX_CHARS` 同值
|
||||
* (`server-rs/crates/module-game-distribution/src/domain.rs:369,377`),按**字符数**计、
|
||||
* 忽略首尾空白;唯一判据仍在服务端(`game_distribution_change_summary_violation`),
|
||||
* 这里只负责「能不能提交」与可读提示,避免两套判据漂移。
|
||||
*/
|
||||
export const CHANGE_SUMMARY_MIN_CHARS = 20;
|
||||
export const CHANGE_SUMMARY_MAX_CHARS = 500;
|
||||
|
||||
/** 衍生作品的核心改动说明校验:返回可读错误(空串 = 通过)。 */
|
||||
export function validateChangeSummary(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return '衍生作品发布新版本必须填写「本次核心改动说明」';
|
||||
}
|
||||
const length = Array.from(trimmed).length;
|
||||
if (length < CHANGE_SUMMARY_MIN_CHARS || length > CHANGE_SUMMARY_MAX_CHARS) {
|
||||
return `「本次核心改动说明」需为 ${CHANGE_SUMMARY_MIN_CHARS} 到 ${CHANGE_SUMMARY_MAX_CHARS} 个字符(当前 ${length})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布失败的展示文案:**保留服务端稳定错误码**(客户端要能按码分支、排障也要看到码),
|
||||
* 核心改动说明的两个码额外给一句可操作说明。
|
||||
*/
|
||||
export function describePublishFailure(error: unknown): string {
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
if (text.includes('FORK_CHANGE_SUMMARY_REQUIRED')) {
|
||||
return `FORK_CHANGE_SUMMARY_REQUIRED:衍生作品发布新版本必须填写「本次核心改动说明」(${CHANGE_SUMMARY_MIN_CHARS}–${CHANGE_SUMMARY_MAX_CHARS} 字)`;
|
||||
}
|
||||
if (text.includes('FORK_CHANGE_SUMMARY_INVALID')) {
|
||||
return `FORK_CHANGE_SUMMARY_INVALID:本次核心改动说明需为 ${CHANGE_SUMMARY_MIN_CHARS} 到 ${CHANGE_SUMMARY_MAX_CHARS} 个字符(按字符计,忽略首尾空白)`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -87,7 +87,9 @@ const FAILURE_PREFIXES: readonly [string, PlatformForkFailureKind][] = [
|
||||
* 前缀是原生错误契约的一部分(`map_fork_source_http_error`),因此这里按前缀精确匹配,
|
||||
* 绝不用文案子串猜测错误类型;未命中前缀的一律按 `other` 原样展示,不吞掉排障信息。
|
||||
*/
|
||||
export function classifyPlatformForkFailure(error: unknown): PlatformForkFailure {
|
||||
export function classifyPlatformForkFailure(
|
||||
error: unknown,
|
||||
): PlatformForkFailure {
|
||||
const raw = error instanceof Error ? error.message : String(error);
|
||||
const text = raw.trim();
|
||||
for (const [prefix, kind] of FAILURE_PREFIXES) {
|
||||
|
||||
@@ -34,8 +34,12 @@ export type GameDistributionPublishMetadata = {
|
||||
inputModes: GameDistributionInputMode[];
|
||||
orientation: GameDistributionOrientation;
|
||||
/**
|
||||
* 上架时的共创授权档位(三态单选,默认禁止共创);只在**首次发布**生效——创建作品以后
|
||||
* 上架时的共创授权档位(三态单选,默认禁止共创)。只在**母版**的首次发布生效:母版创建后
|
||||
* 只能通过网页端「我的作品」单向提升,更新版本不会改动它。
|
||||
*
|
||||
* 衍生作品(项目里有 `.agent/fork-source.json`)传了也会被服务端忽略——档位在创建时由父作品
|
||||
* 快照继承,之后任何提升都被拒绝(`FORK_AUTHORIZATION_INHERITED`),所以面板对衍生作品根本
|
||||
* 不渲染三态选择。这里保留字段只是为了母版链路不变。
|
||||
*/
|
||||
forkAuthorization?: GameDistributionForkAuthorization;
|
||||
};
|
||||
@@ -367,6 +371,13 @@ export async function publishLocalProjectGame(args: {
|
||||
* 缺省即「授权非禁止时上传」;`false` 表示作者主动取消——源码不外发,作品只能被产物级改编。
|
||||
*/
|
||||
includeProjectBundle?: boolean;
|
||||
/**
|
||||
* 衍生作品的「本次核心改动说明」(母版不需要)。
|
||||
*
|
||||
* 服务端口径:衍生作品的版本必填,trim 后 20–500 个字符;这里原样透传,不做本地判据(校验只有
|
||||
* 服务端那一份,客户端只负责采集与给可读提示)。
|
||||
*/
|
||||
changeSummary?: string;
|
||||
}): Promise<GameDistributionPublishResult> {
|
||||
const projectPath = args.projectPath.trim();
|
||||
const packageRelativePath = args.packageRelativePath.trim();
|
||||
@@ -425,6 +436,10 @@ export async function publishLocalProjectGame(args: {
|
||||
...(args.includeProjectBundle === false
|
||||
? { includeProjectBundle: false }
|
||||
: {}),
|
||||
// 衍生作品的核心改动说明:原样透传(服务端负责 trim、必填与长度判据)。
|
||||
...(typeof args.changeSummary === 'string' && args.changeSummary.trim()
|
||||
? { changeSummary: args.changeSummary.trim() }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,6 +153,37 @@ describe('Rust 发布 facade', () => {
|
||||
expect(calls[1]?.args).not.toHaveProperty('includeProjectBundle');
|
||||
});
|
||||
|
||||
test('衍生作品的核心改动说明 trim 后透传,母版留空不带字段', async () => {
|
||||
const { invoke, calls } = installNativeInvoke(() => PUBLISH_RESULT);
|
||||
|
||||
// 衍生作品的必填与 20–500 字符判据都在服务端;渲染层只做 trim 透传。
|
||||
await publishLocalProjectGame({
|
||||
invoke,
|
||||
projectPath: '/tmp/project',
|
||||
packageRelativePath: 'exports/playtest-package-1.zip',
|
||||
manifest: MANIFEST,
|
||||
metadata: { coverAssetId: 'asset_cover' },
|
||||
versionNumber: 2,
|
||||
changeSummary:
|
||||
' 把跳台改成三段,并重画全部背景与手感,还新增了两个关卡 ',
|
||||
});
|
||||
expect(calls[0]?.args).toMatchObject({
|
||||
changeSummary: '把跳台改成三段,并重画全部背景与手感,还新增了两个关卡',
|
||||
});
|
||||
|
||||
// 空串/纯空白视为没写:不带字段(母版本来也不该带),由服务端对衍生作品回必填码。
|
||||
await publishLocalProjectGame({
|
||||
invoke,
|
||||
projectPath: '/tmp/project',
|
||||
packageRelativePath: 'exports/playtest-package-1.zip',
|
||||
manifest: MANIFEST,
|
||||
metadata: { coverAssetId: 'asset_cover' },
|
||||
versionNumber: 2,
|
||||
changeSummary: ' ',
|
||||
});
|
||||
expect(calls[1]?.args).not.toHaveProperty('changeSummary');
|
||||
});
|
||||
|
||||
test('共创授权档位随创建作品请求提交,改编声明不来自渲染层', async () => {
|
||||
const { invoke, calls } = installNativeInvoke(() => PUBLISH_RESULT);
|
||||
|
||||
|
||||
@@ -560,6 +560,141 @@ describe('GameDistributionPublishPanel', () => {
|
||||
expect(warning.textContent).toContain('只能被产物级改编');
|
||||
});
|
||||
|
||||
test('衍生作品:核心改动说明必填,授权只读继承父作品', async () => {
|
||||
const invoke = installTauriInvoke(async (command: string) => {
|
||||
if (command === 'read_local_project_fork_source') {
|
||||
return { gameId: 'game_parent_1', versionId: 'gamever_parent_1' };
|
||||
}
|
||||
if (command === 'read_public_game_detail') {
|
||||
return { id: 'game_parent_1', forkAuthorization: 'nonCommercial' };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
vi.mocked(publishLocalProjectGame).mockResolvedValue({
|
||||
gameId: 'game_1',
|
||||
versionId: 'gamever_1',
|
||||
versionNumber: 1,
|
||||
status: 'pending_review',
|
||||
packageSha256: 'a'.repeat(64),
|
||||
packageSizeBytes: 3,
|
||||
fileCount: 2,
|
||||
});
|
||||
renderPanel();
|
||||
|
||||
// 授权只读,且没有三态单选;核心改动说明在场。
|
||||
expect(
|
||||
await screen.findByText(/授权继承自父作品:允许非商用共创/u),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByRole('radiogroup', { name: '授权共创' })).toBeNull();
|
||||
const summaryInput = screen.getByLabelText('本次核心改动说明');
|
||||
expect(summaryInput).not.toBeNull();
|
||||
|
||||
// 没填就提交:本地拦下,不发发布请求。
|
||||
await selectCover();
|
||||
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
|
||||
expect(publishLocalProjectGame).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await screen.findByText(/必须填写「本次核心改动说明」/u),
|
||||
).not.toBeNull();
|
||||
|
||||
// 填够 20 字才允许提交,并把 trim 后的值透传给原生。
|
||||
fireEvent.change(summaryInput, {
|
||||
target: {
|
||||
value: ' 把跳台改成三段,并重画全部背景与手感,还新增了两个关卡 ',
|
||||
},
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
|
||||
await waitFor(() =>
|
||||
expect(publishLocalProjectGame).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
expect(vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0]).toMatchObject(
|
||||
{
|
||||
changeSummary: '把跳台改成三段,并重画全部背景与手感,还新增了两个关卡',
|
||||
},
|
||||
);
|
||||
expect(invoke).toHaveBeenCalledWith('read_local_project_fork_source', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
});
|
||||
});
|
||||
|
||||
test('衍生作品:核心改动说明不足 20 字也拦下并给可读提示', async () => {
|
||||
installTauriInvoke(async (command: string) =>
|
||||
command === 'read_local_project_fork_source'
|
||||
? { gameId: 'game_parent_1', versionId: 'gamever_parent_1' }
|
||||
: undefined,
|
||||
);
|
||||
renderPanel();
|
||||
|
||||
const summaryInput = await screen.findByLabelText('本次核心改动说明');
|
||||
fireEvent.change(summaryInput, { target: { value: '改了点' } });
|
||||
await selectCover();
|
||||
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
|
||||
|
||||
expect(publishLocalProjectGame).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await screen.findByText(/需为 20 到 500 个字符(当前 3)/u),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test('母版:不显示核心改动说明,授权三态仍可选', async () => {
|
||||
installTauriInvoke(async (command: string) =>
|
||||
command === 'read_local_project_fork_source' ? null : undefined,
|
||||
);
|
||||
vi.mocked(publishLocalProjectGame).mockResolvedValue({
|
||||
gameId: 'game_1',
|
||||
versionId: 'gamever_1',
|
||||
versionNumber: 1,
|
||||
status: 'pending_review',
|
||||
packageSha256: 'a'.repeat(64),
|
||||
packageSizeBytes: 3,
|
||||
fileCount: 2,
|
||||
});
|
||||
renderPanel();
|
||||
|
||||
expect(
|
||||
await screen.findByRole('radiogroup', { name: '授权共创' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByLabelText('本次核心改动说明')).toBeNull();
|
||||
|
||||
await selectCover();
|
||||
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
|
||||
await waitFor(() =>
|
||||
expect(publishLocalProjectGame).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
// 母版不带核心改动说明(服务端对母版忽略该字段)。
|
||||
expect(
|
||||
vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0],
|
||||
).not.toHaveProperty('changeSummary');
|
||||
});
|
||||
|
||||
test('核心改动说明的服务端稳定码原样展示', async () => {
|
||||
installTauriInvoke(async (command: string) =>
|
||||
command === 'read_local_project_fork_source'
|
||||
? { gameId: 'game_parent_1', versionId: 'gamever_parent_1' }
|
||||
: undefined,
|
||||
);
|
||||
vi.mocked(publishLocalProjectGame).mockRejectedValue(
|
||||
new Error(
|
||||
'FORK_CHANGE_SUMMARY_INVALID: 本次核心改动说明需为 20 到 500 个字符(按字符计,忽略首尾空白)',
|
||||
),
|
||||
);
|
||||
renderPanel();
|
||||
|
||||
const summaryInput = await screen.findByLabelText('本次核心改动说明');
|
||||
fireEvent.change(summaryInput, {
|
||||
target: {
|
||||
value: '把跳台改成三段,并重画全部背景与手感,还新增了两个关卡',
|
||||
},
|
||||
});
|
||||
await selectCover();
|
||||
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
|
||||
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(alert.textContent).toContain('FORK_CHANGE_SUMMARY_INVALID');
|
||||
expect(alert.textContent).toContain('20');
|
||||
expect(alert.textContent).toContain('500');
|
||||
});
|
||||
|
||||
test('更新发布时共创授权只读并给出可操作提示', async () => {
|
||||
installTauriInvoke(async () => undefined);
|
||||
vi.mocked(readGameDistributionPublication).mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user