发布入口接入灰度开关
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 smoke (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

- 后端:GET /api/runtime/frontend-config 新增 gameDistributionPublishEnabled,复用 `game-distribution:publish` 判据(未配置或 enabled=false 时对已登录作者默认开放,显式收紧后只放行白名单/灰度命中用户,匿名恒为 false),前端入口与写入口共用同一事实源。
- 后端用例:新增 frontend_runtime_config_game_distribution_publish_is_scoped_to_authenticated_gate,覆盖默认开放、enabled=true 无白名单、白名单命中、deny 名单、enabled=false 回退与 rolloutPercent=100。
- 网页:平台壳按灰度隐藏「发布游戏 / 发布新版本」入口;/games/publish 直接访问时渲染「发布功能正在灰度中」并提供重新检查;读取失败按放行处理,由后端写入口把关并返回可读文案。
- AGC:新增 readGamePublishAvailability(同一运行时配置字段),只有命中才把发布回调交给 DirectProject 聊天头;字段缺失或读取失败按不开放处理。
- 文档:玩法链路、后端数据契约与实施计划记录灰度口径、入口行为与验证命令。
This commit is contained in:
2026-09-22 17:12:04 +08:00
parent 1bd5fe9ff0
commit e5460280ce
13 changed files with 396 additions and 45 deletions
@@ -58,6 +58,15 @@ const gameDistributionMock = vi.hoisted(() => ({
vi.mock('../../services/gameDistributionClient', () => gameDistributionMock);
const frontendRuntimeConfigMock = vi.hoisted(() => ({
loadFrontendRuntimeConfig: vi.fn(),
}));
vi.mock('../../services/frontendRuntimeConfigService', () => ({
loadFrontendRuntimeConfig: (...args: unknown[]) =>
frontendRuntimeConfigMock.loadFrontendRuntimeConfig(...args),
}));
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => authUiMock.value,
}));
@@ -206,6 +215,12 @@ describe('PlatformEntryActiveFlowShell', () => {
for (const mock of Object.values(gameDistributionMock)) {
mock.mockReset();
}
frontendRuntimeConfigMock.loadFrontendRuntimeConfig.mockReset();
frontendRuntimeConfigMock.loadFrontendRuntimeConfig.mockResolvedValue({
imageEditorAgentSidebarEnabled: false,
agcTemplateLibraryEnabled: false,
gameDistributionPublishEnabled: true,
});
authUiMock.value.openLoginModal.mockReset();
responsiveMock.isDesktopLayout = true;
});
@@ -850,3 +865,47 @@ describe('PlatformEntryActiveFlowShell', () => {
});
});
});
describe('游戏发布灰度入口', () => {
function loginAsAuthor() {
authUiMock.value.user = {
id: 'user-1',
publicUserCode: '100001',
displayName: '测试作者',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
};
authUiMock.value.canAccessProtectedData = true;
}
it('灰度未命中时广场不展示发布入口', async () => {
loginAsAuthor();
frontendRuntimeConfigMock.loadFrontendRuntimeConfig.mockResolvedValue({
imageEditorAgentSidebarEnabled: false,
agcTemplateLibraryEnabled: false,
gameDistributionPublishEnabled: false,
});
gameDistributionMock.listGames.mockResolvedValue([]);
render(<StatefulPlatformEntryFlowShell initialStage="games" />);
expect(await screen.findByText('游戏广场')).toBeTruthy();
await waitFor(() =>
expect(screen.queryByRole('button', { name: '发布游戏' })).toBeNull(),
);
});
it('灰度命中时广场展示发布入口', async () => {
loginAsAuthor();
gameDistributionMock.listGames.mockResolvedValue([]);
render(<StatefulPlatformEntryFlowShell initialStage="games" />);
expect(
await screen.findByRole('button', { name: '发布游戏' }),
).not.toBeNull();
});
});