diff --git a/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx b/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx index ac4bb8e80..7bf4b5499 100644 --- a/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx +++ b/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx @@ -175,6 +175,86 @@ test('灰度发布页可选择模板库并默认启用零比例灰度', async () ); }); +test('灰度发布页可选择游戏发布开关,默认保持「未开启即开放」语义', async () => { + const user = userEvent.setup(); + render( + , + ); + + await screen.findByRole('button', { name: 'editor.new-toolbar' }); + await user.selectOptions(screen.getByLabelText('Gate Key 前缀'), [ + 'game-distribution', + ]); + + expect((screen.getByLabelText('Gate Key') as HTMLInputElement).value).toBe( + 'game-distribution:publish', + ); + expect( + (screen.getByLabelText('Gate Key 目标') as HTMLSelectElement).value, + ).toBe('publish'); + // 该开关的语义是「未配置/关闭 = 默认开放」,所以选中后不能默认打开收紧。 + expect((screen.getByLabelText('启用') as HTMLInputElement).checked).toBe( + false, + ); + expect((screen.getByLabelText('灰度比例') as HTMLInputElement).value).toBe( + '0', + ); + expect( + (screen.getByLabelText('描述') as HTMLTextAreaElement).value, + ).toContain('游戏发布入口灰度'); +}); + +test('灰度发布页保存游戏发布开关时写入白名单与比例', async () => { + const user = userEvent.setup(); + vi.mocked(upsertAdminFeatureGateConfig).mockResolvedValueOnce({ + gates: [ + ...configResponse.gates, + { + gateKey: 'game-distribution:publish', + enabled: true, + rolloutPercent: 20, + allowUserIds: ['user-internal'], + allowUserTags: [], + denyUserIds: [], + description: '游戏发布入口灰度', + updatedAt: '2026-09-22T10:00:00Z', + }, + ], + }); + render( + , + ); + + await screen.findByRole('button', { name: 'editor.new-toolbar' }); + await user.selectOptions(screen.getByLabelText('Gate Key 前缀'), [ + 'game-distribution', + ]); + fireEvent.click(screen.getByLabelText('启用')); + fireEvent.change(screen.getByLabelText('灰度比例'), { + target: { value: '20' }, + }); + fireEvent.change(screen.getByLabelText('允许用户 ID'), { + target: { value: 'user-internal' }, + }); + fireEvent.change(screen.getByLabelText('描述'), { + target: { value: '游戏发布入口灰度' }, + }); + await user.click(screen.getByRole('button', { name: '保存配置' })); + await user.click(screen.getByRole('button', { name: '确认' })); + + await waitFor(() => + expect(upsertAdminFeatureGateConfig).toHaveBeenCalledWith('admin-token', { + gateKey: 'game-distribution:publish', + enabled: true, + rolloutPercent: 20, + allowUserIds: ['user-internal'], + allowUserTags: [], + denyUserIds: [], + description: '游戏发布入口灰度', + }), + ); +}); + test('灰度发布页保存时转换数组和百分比', async () => { const user = userEvent.setup(); vi.mocked(upsertAdminFeatureGateConfig).mockResolvedValueOnce({ diff --git a/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx b/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx index 814f2ac34..f90e51a48 100644 --- a/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx +++ b/apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx @@ -28,6 +28,7 @@ interface GateTargetOption { const GATE_PREFIX_LABELS: Record = { 'image-editor': '画布', agc: '客户端', + 'game-distribution': '游戏分发', }; const FIXED_GATE_TARGETS: GateTargetOption[] = [ @@ -45,6 +46,14 @@ const FIXED_GATE_TARGETS: GateTargetOption[] = [ label: 'Agent 侧边栏', description: '画布 Agent 入口灰度', }, + { + prefix: 'game-distribution', + suffix: 'publish', + key: 'game-distribution:publish', + label: '游戏发布', + description: + '游戏发布入口灰度:未配置或关闭时对已登录作者默认开放,开启后只放行白名单 / 灰度命中', + }, ]; export function AdminGrayReleaseConfigPage({ diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 3aa30b5be..1e4181093 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -678,7 +678,7 @@ journalctl -u genarrative-api -o cat | grep 'operation="release_rejected"' 发布事故或回滚窗口里用 `game-distribution:publish` 灰度开关控制写入,不需要改代码或重启: -- 开关位置:后台「灰度发布配置」(`GET/PUT /admin/api/feature-gates`),`gateKey = game-distribution:publish`。 +- 开关位置:后台「灰度发布配置」(`GET/PUT /admin/api/feature-gates`),`gateKey = game-distribution:publish`;后台预设「游戏分发 → 游戏发布」,选中后默认保持 `enabled=false`(即默认开放),需要收紧时再显式打开并填白名单 / 灰度比例。该键在配置前不会出现在已有开关列表里,必须从预设或手填 Gate Key 新建一行。 - 语义:没有该 gate 行或 `enabled=false` 表示**默认开放**;`enabled=true` 时只有 `allowUserIds` / `allowUserTags` / `rolloutPercent` 命中的作者能发布,`rolloutPercent=0` 且无白名单即**全部关闭**(等价紧急关闭投稿)。 - 关闭范围:创建游戏、创建版本、上传包、送审、撤回、作者下架,以及管理员**批准**(新版本激活)都返回 `503 GAME_DISTRIBUTION_PUBLISH_DISABLED`。 - 始终可用:目录、详情、版本回读、发行网关(已公开游戏继续游玩)、`/my-games`、审核队列读取、**拒绝审核**与管理员**安全下架**。