diff --git a/admin/src/PlaytestDetailPage.test.tsx b/admin/src/PlaytestDetailPage.test.tsx index 75763a5..c72991c 100644 --- a/admin/src/PlaytestDetailPage.test.tsx +++ b/admin/src/PlaytestDetailPage.test.tsx @@ -23,6 +23,7 @@ const mockGetAdtLinkages = vi.fn() const mockGetAdtGames = vi.fn() const mockGetAdtBuilds = vi.fn() const mockChangeAdtBuild = vi.fn() +const mockCheckAdtBuild = vi.fn() const mockApprove = vi.fn() const mockReject = vi.fn() const mockRetryDm = vi.fn() @@ -49,7 +50,8 @@ vi.mock('./playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query' Survey_ByPlaytestId: 'survey-by-playtest-id', GamesAdt_ByAdtLinkageId: 'adt-games-by-linkage-id', BuildsAdt_ByAdtLinkageId: 'adt-builds-by-linkage-id', - AdtBuildChange_ByPlaytestId: 'adt-build-change-by-playtest-id' + AdtBuildChange_ByPlaytestId: 'adt-build-change-by-playtest-id', + AdtBuildCheck_ByPlaytestId: 'adt-build-check-by-playtest-id' }, usePlaytesthubServiceAdminApi_GetPlaytests: (...a: unknown[]) => mockGetPlaytests(...a), usePlaytesthubServiceAdminApi_CreatePlaytest_ByPlaytestIdTransitionStatuMutation: (...a: unknown[]) => @@ -67,6 +69,7 @@ vi.mock('./playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query' usePlaytesthubServiceAdminApi_GetGamesAdt_ByAdtLinkageId: (...a: unknown[]) => mockGetAdtGames(...a), usePlaytesthubServiceAdminApi_GetBuildsAdt_ByAdtLinkageId: (...a: unknown[]) => mockGetAdtBuilds(...a), usePlaytesthubServiceAdminApi_CreateAdtBuildChange_ByPlaytestIdMutation: (...a: unknown[]) => mockChangeAdtBuild(...a), + usePlaytesthubServiceAdminApi_CreateAdtBuildCheck_ByPlaytestIdMutation: (...a: unknown[]) => mockCheckAdtBuild(...a), usePlaytesthubServiceAdminApi_CreateApplicant_ByApplicantIdApproveMutation: (...a: unknown[]) => mockApprove(...a), usePlaytesthubServiceAdminApi_CreateApplicant_ByApplicantIdRejectMutation: (...a: unknown[]) => mockReject(...a), @@ -130,6 +133,7 @@ beforeEach(() => { mockGetAdtGames.mockReturnValue({ data: { games: [] }, isLoading: false, error: null }) mockGetAdtBuilds.mockReturnValue({ data: { builds: [] }, isLoading: false, error: null }) mockChangeAdtBuild.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false, error: null }) + mockCheckAdtBuild.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false, error: null }) mockApprove.mockReturnValue({ mutate: vi.fn(), isPending: false }) mockReject.mockReturnValue({ mutate: vi.fn(), isPending: false }) mockRetryDm.mockReturnValue({ mutate: vi.fn(), isPending: false }) diff --git a/admin/src/playtesthubapi/generated-admin/PlaytesthubServiceAdminApi.ts b/admin/src/playtesthubapi/generated-admin/PlaytesthubServiceAdminApi.ts index 1971576..6434b6c 100644 --- a/admin/src/playtesthubapi/generated-admin/PlaytesthubServiceAdminApi.ts +++ b/admin/src/playtesthubapi/generated-admin/PlaytesthubServiceAdminApi.ts @@ -11,6 +11,7 @@ import { ApiUtils, Network } from '@accelbyte/sdk' import type { AxiosRequestConfig, AxiosResponse } from 'axios' import { PlaytesthubServiceApproveApplicantBody } from '../generated-definitions/PlaytesthubServiceApproveApplicantBody.js' import { PlaytesthubServiceChangeAdtBuildBody } from '../generated-definitions/PlaytesthubServiceChangeAdtBuildBody.js' +import { PlaytesthubServiceCheckAdtBuildBody } from '../generated-definitions/PlaytesthubServiceCheckAdtBuildBody.js' import { PlaytesthubServiceCompleteAdtLinkBody } from '../generated-definitions/PlaytesthubServiceCompleteAdtLinkBody.js' import { PlaytesthubServiceCreateAnnouncementBody } from '../generated-definitions/PlaytesthubServiceCreateAnnouncementBody.js' import { PlaytesthubServiceCreatePlaytestBody } from '../generated-definitions/PlaytesthubServiceCreatePlaytestBody.js' @@ -29,6 +30,7 @@ import { PlaytesthubServiceUploadCodesBody } from '../generated-definitions/Play import { V1AdminGetPlaytestResponse } from '../generated-definitions/V1AdminGetPlaytestResponse.js' import { V1ApproveApplicantResponse } from '../generated-definitions/V1ApproveApplicantResponse.js' import { V1ChangeAdtBuildResponse } from '../generated-definitions/V1ChangeAdtBuildResponse.js' +import { V1CheckAdtBuildResponse } from '../generated-definitions/V1CheckAdtBuildResponse.js' import { V1CompleteAdtLinkResponse } from '../generated-definitions/V1CompleteAdtLinkResponse.js' import { V1CreateAnnouncementResponse } from '../generated-definitions/V1CreateAnnouncementResponse.js' import { V1CreatePlaytestResponse } from '../generated-definitions/V1CreatePlaytestResponse.js' @@ -330,6 +332,16 @@ export function PlaytesthubServiceAdminApi(sdk: AccelByteSDK, args?: SdkSetConfi return resp.response } + async function createAdtBuildCheck_ByPlaytestId( + playtestId: string, + data: PlaytesthubServiceCheckAdtBuildBody + ): Promise> { + const $ = new PlaytesthubServiceAdmin$(axiosInstance, namespace, useSchemaValidation) + const resp = await $.createAdtBuildCheck_ByPlaytestId(playtestId, data) + if (resp.error) throw resp.error + return resp.response + } + async function createPlaytest_ByPlaytestIdTransitionStatu( playtestId: string, data: PlaytesthubServiceTransitionPlaytestStatusBody @@ -482,6 +494,10 @@ export function PlaytesthubServiceAdminApi(sdk: AccelByteSDK, args?: SdkSetConfi * Resolves recipients at call time (NOT a stored snapshot). Subject + message are PII-sensitive and are never written to audit JSONB or structured logs. */ createAnnouncement_ByPlaytestId, + /** + * Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success. + */ + createAdtBuildCheck_ByPlaytestId, createPlaytest_ByPlaytestIdTransitionStatu, /** diff --git a/admin/src/playtesthubapi/generated-admin/endpoints/PlaytesthubServiceAdmin$.ts b/admin/src/playtesthubapi/generated-admin/endpoints/PlaytesthubServiceAdmin$.ts index 377b8e4..69901ee 100644 --- a/admin/src/playtesthubapi/generated-admin/endpoints/PlaytesthubServiceAdmin$.ts +++ b/admin/src/playtesthubapi/generated-admin/endpoints/PlaytesthubServiceAdmin$.ts @@ -11,6 +11,7 @@ import { Validate } from '@accelbyte/sdk' import type { AxiosInstance, AxiosRequestConfig } from 'axios' import { PlaytesthubServiceApproveApplicantBody } from '../../generated-definitions/PlaytesthubServiceApproveApplicantBody.js' import { PlaytesthubServiceChangeAdtBuildBody } from '../../generated-definitions/PlaytesthubServiceChangeAdtBuildBody.js' +import { PlaytesthubServiceCheckAdtBuildBody } from '../../generated-definitions/PlaytesthubServiceCheckAdtBuildBody.js' import { PlaytesthubServiceCompleteAdtLinkBody } from '../../generated-definitions/PlaytesthubServiceCompleteAdtLinkBody.js' import { PlaytesthubServiceCreateAnnouncementBody } from '../../generated-definitions/PlaytesthubServiceCreateAnnouncementBody.js' import { PlaytesthubServiceCreatePlaytestBody } from '../../generated-definitions/PlaytesthubServiceCreatePlaytestBody.js' @@ -29,6 +30,7 @@ import { PlaytesthubServiceUploadCodesBody } from '../../generated-definitions/P import { V1AdminGetPlaytestResponse } from '../../generated-definitions/V1AdminGetPlaytestResponse.js' import { V1ApproveApplicantResponse } from '../../generated-definitions/V1ApproveApplicantResponse.js' import { V1ChangeAdtBuildResponse } from '../../generated-definitions/V1ChangeAdtBuildResponse.js' +import { V1CheckAdtBuildResponse } from '../../generated-definitions/V1CheckAdtBuildResponse.js' import { V1CompleteAdtLinkResponse } from '../../generated-definitions/V1CompleteAdtLinkResponse.js' import { V1CreateAnnouncementResponse } from '../../generated-definitions/V1CreateAnnouncementResponse.js' import { V1CreatePlaytestResponse } from '../../generated-definitions/V1CreatePlaytestResponse.js' @@ -504,6 +506,26 @@ export class PlaytesthubServiceAdmin$ { 'V1CreateAnnouncementResponse' ) } + /** + * Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success. + */ + createAdtBuildCheck_ByPlaytestId( + playtestId: string, + data: PlaytesthubServiceCheckAdtBuildBody + ): Promise> { + const params = {} as AxiosRequestConfig + const url = '/v1/admin/namespaces/{namespace}/playtests/{playtestId}/adt/build:check' + .replace('{namespace}', this.namespace) + .replace('{playtestId}', playtestId) + const resultPromise = this.axiosInstance.post(url, data, { params }) + + return Validate.validateOrReturnResponse( + this.useSchemaValidation, + () => resultPromise, + V1CheckAdtBuildResponse, + 'V1CheckAdtBuildResponse' + ) + } createPlaytest_ByPlaytestIdTransitionStatu( playtestId: string, diff --git a/admin/src/playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query.ts b/admin/src/playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query.ts index feac992..64354b1 100644 --- a/admin/src/playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query.ts +++ b/admin/src/playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query.ts @@ -14,6 +14,7 @@ import { PlaytesthubServiceAdminApi } from '../PlaytesthubServiceAdminApi.js' import { PlaytesthubServiceApproveApplicantBody } from '../../generated-definitions/PlaytesthubServiceApproveApplicantBody.js' import { PlaytesthubServiceChangeAdtBuildBody } from '../../generated-definitions/PlaytesthubServiceChangeAdtBuildBody.js' +import { PlaytesthubServiceCheckAdtBuildBody } from '../../generated-definitions/PlaytesthubServiceCheckAdtBuildBody.js' import { PlaytesthubServiceCompleteAdtLinkBody } from '../../generated-definitions/PlaytesthubServiceCompleteAdtLinkBody.js' import { PlaytesthubServiceCreateAnnouncementBody } from '../../generated-definitions/PlaytesthubServiceCreateAnnouncementBody.js' import { PlaytesthubServiceCreatePlaytestBody } from '../../generated-definitions/PlaytesthubServiceCreatePlaytestBody.js' @@ -32,6 +33,7 @@ import { PlaytesthubServiceUploadCodesBody } from '../../generated-definitions/P import { V1AdminGetPlaytestResponse } from '../../generated-definitions/V1AdminGetPlaytestResponse.js' import { V1ApproveApplicantResponse } from '../../generated-definitions/V1ApproveApplicantResponse.js' import { V1ChangeAdtBuildResponse } from '../../generated-definitions/V1ChangeAdtBuildResponse.js' +import { V1CheckAdtBuildResponse } from '../../generated-definitions/V1CheckAdtBuildResponse.js' import { V1CompleteAdtLinkResponse } from '../../generated-definitions/V1CompleteAdtLinkResponse.js' import { V1CreateAnnouncementResponse } from '../../generated-definitions/V1CreateAnnouncementResponse.js' import { V1CreatePlaytestResponse } from '../../generated-definitions/V1CreatePlaytestResponse.js' @@ -87,6 +89,7 @@ export const Key_PlaytesthubServiceAdmin = { Participants_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.Participants_ByPlaytestId', Announcements_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.Announcements_ByPlaytestId', Announcement_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.Announcement_ByPlaytestId', + AdtBuildCheck_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.AdtBuildCheck_ByPlaytestId', Playtest_ByPlaytestIdTransitionStatu: 'Playtesthubapi.PlaytesthubServiceAdmin.Playtest_ByPlaytestIdTransitionStatu', AdtBuildChange_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.AdtBuildChange_ByPlaytestId', SurveyResponses_ByPlaytestId: 'Playtesthubapi.PlaytesthubServiceAdmin.SurveyResponses_ByPlaytestId', @@ -1092,6 +1095,49 @@ export const usePlaytesthubServiceAdminApi_CreateAnnouncement_ByPlaytestIdMutati }) } +/** + * Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success. + * + * #### Default Query Options + * The default options include: + * ``` + * { + * queryKey: [Key_PlaytesthubServiceAdmin.AdtBuildCheck_ByPlaytestId, input] + * } + * ``` + */ +export const usePlaytesthubServiceAdminApi_CreateAdtBuildCheck_ByPlaytestIdMutation = ( + sdk: AccelByteSDK, + options?: Omit< + UseMutationOptions< + V1CheckAdtBuildResponse, + AxiosError, + SdkSetConfigParam & { playtestId: string; data: PlaytesthubServiceCheckAdtBuildBody } + >, + 'mutationKey' + >, + callback?: (data: V1CheckAdtBuildResponse) => void +): UseMutationResult< + V1CheckAdtBuildResponse, + AxiosError, + SdkSetConfigParam & { playtestId: string; data: PlaytesthubServiceCheckAdtBuildBody } +> => { + const mutationFn = async (input: SdkSetConfigParam & { playtestId: string; data: PlaytesthubServiceCheckAdtBuildBody }) => { + const response = await PlaytesthubServiceAdminApi(sdk, { + coreConfig: input.coreConfig, + axiosConfig: input.axiosConfig + }).createAdtBuildCheck_ByPlaytestId(input.playtestId, input.data) + callback?.(response.data) + return response.data + } + + return useMutation({ + mutationKey: [Key_PlaytesthubServiceAdmin.AdtBuildCheck_ByPlaytestId], + mutationFn, + ...options + }) +} + export const usePlaytesthubServiceAdminApi_CreatePlaytest_ByPlaytestIdTransitionStatuMutation = ( sdk: AccelByteSDK, options?: Omit< diff --git a/admin/src/playtesthubapi/generated-definitions/PlaytesthubServiceCheckAdtBuildBody.ts b/admin/src/playtesthubapi/generated-definitions/PlaytesthubServiceCheckAdtBuildBody.ts new file mode 100644 index 0000000..1f3019b --- /dev/null +++ b/admin/src/playtesthubapi/generated-definitions/PlaytesthubServiceCheckAdtBuildBody.ts @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022-2026 AccelByte Inc. All Rights Reserved + * This is licensed software from AccelByte Inc, for limitations + * and restrictions contact your company contract manager. + */ +import { z } from 'zod' + +export const PlaytesthubServiceCheckAdtBuildBody = z.any() + +export interface PlaytesthubServiceCheckAdtBuildBody extends z.TypeOf {} diff --git a/admin/src/playtesthubapi/generated-definitions/V1CheckAdtBuildResponse.ts b/admin/src/playtesthubapi/generated-definitions/V1CheckAdtBuildResponse.ts new file mode 100644 index 0000000..559f1f0 --- /dev/null +++ b/admin/src/playtesthubapi/generated-definitions/V1CheckAdtBuildResponse.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2022-2026 AccelByte Inc. All Rights Reserved + * This is licensed software from AccelByte Inc, for limitations + * and restrictions contact your company contract manager. + */ +import { z } from 'zod' +import { V1Playtest } from './V1Playtest.js' + +export const V1CheckAdtBuildResponse = z.object({ playtest: V1Playtest.nullish(), healthy: z.boolean().nullish() }) + +export interface V1CheckAdtBuildResponse extends z.TypeOf {} diff --git a/admin/src/playtesthubapi/generated-definitions/V1Playtest.ts b/admin/src/playtesthubapi/generated-definitions/V1Playtest.ts index 3cef3b7..a135306 100644 --- a/admin/src/playtesthubapi/generated-definitions/V1Playtest.ts +++ b/admin/src/playtesthubapi/generated-definitions/V1Playtest.ts @@ -35,7 +35,9 @@ export const V1Playtest = z.object({ adtNamespace: z.string().nullish(), adtGameId: z.string().nullish(), adtBuildId: z.string().nullish(), - adtFallbackDownloadUrl: z.string().nullish() + adtFallbackDownloadUrl: z.string().nullish(), + adtBuildStatus: z.string().nullish(), + adtBuildCheckedAt: z.string().nullish() }) export interface V1Playtest extends z.TypeOf {} diff --git a/admin/src/tabs/DistributionTab.test.tsx b/admin/src/tabs/DistributionTab.test.tsx index 85d2276..32c7035 100644 --- a/admin/src/tabs/DistributionTab.test.tsx +++ b/admin/src/tabs/DistributionTab.test.tsx @@ -16,6 +16,7 @@ const mockGetAdtLinkages = vi.fn() const mockGetAdtGames = vi.fn() const mockGetAdtBuilds = vi.fn() const mockChangeAdtBuild = vi.fn() +const mockCheckAdtBuild = vi.fn() vi.mock('../playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query', () => ({ Key_PlaytesthubServiceAdmin: { @@ -23,7 +24,8 @@ vi.mock('../playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query Codes_ByPlaytestId: 'codes-by-playtest-id', GamesAdt_ByAdtLinkageId: 'adt-games-by-linkage-id', BuildsAdt_ByAdtLinkageId: 'adt-builds-by-linkage-id', - AdtBuildChange_ByPlaytestId: 'adt-build-change-by-playtest-id' + AdtBuildChange_ByPlaytestId: 'adt-build-change-by-playtest-id', + AdtBuildCheck_ByPlaytestId: 'adt-build-check-by-playtest-id' }, usePlaytesthubServiceAdminApi_GetCodes_ByPlaytestId: (...a: unknown[]) => mockGetCodes(...a), usePlaytesthubServiceAdminApi_CreateCodesUpload_ByPlaytestIdMutation: (...a: unknown[]) => mockUploadCodes(...a), @@ -32,7 +34,8 @@ vi.mock('../playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query usePlaytesthubServiceAdminApi_GetAdtLinkages: (...a: unknown[]) => mockGetAdtLinkages(...a), usePlaytesthubServiceAdminApi_GetGamesAdt_ByAdtLinkageId: (...a: unknown[]) => mockGetAdtGames(...a), usePlaytesthubServiceAdminApi_GetBuildsAdt_ByAdtLinkageId: (...a: unknown[]) => mockGetAdtBuilds(...a), - usePlaytesthubServiceAdminApi_CreateAdtBuildChange_ByPlaytestIdMutation: (...a: unknown[]) => mockChangeAdtBuild(...a) + usePlaytesthubServiceAdminApi_CreateAdtBuildChange_ByPlaytestIdMutation: (...a: unknown[]) => mockChangeAdtBuild(...a), + usePlaytesthubServiceAdminApi_CreateAdtBuildCheck_ByPlaytestIdMutation: (...a: unknown[]) => mockCheckAdtBuild(...a) })) // Stub the build picker so its onPick prop can be driven deterministically @@ -84,6 +87,7 @@ beforeEach(() => { mockGetAdtGames.mockReturnValue({ data: { games: [{ id: 'game-1', name: 'Starfield Dev' }] }, isLoading: false, error: null }) mockGetAdtBuilds.mockReturnValue({ data: { builds: [] }, isLoading: false, error: null }) mockChangeAdtBuild.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false, error: null }) + mockCheckAdtBuild.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false, error: null }) }) describe('ADTPanel (Change Build)', () => { @@ -113,3 +117,31 @@ describe('ADTPanel (Change Build)', () => { ) }) }) + +describe('ADTPanel (build health surfacing)', () => { + it('shows no health alert before the build has been checked', () => { + renderTab(ADT_PT) + expect(screen.queryByTestId('adt-build-health-alert')).not.toBeInTheDocument() + }) + + it('surfaces an error alert when the build is UNAVAILABLE', () => { + renderTab({ ...ADT_PT, adtBuildStatus: 'UNAVAILABLE', adtBuildCheckedAt: '2026-05-26T10:00:00Z' }) + const alert = screen.getByTestId('adt-build-health-alert') + expect(alert).toHaveTextContent(/no longer available in ADT/i) + expect(alert).toHaveTextContent(/fallback download URL or change the build/i) + }) + + it('surfaces a success alert when the build is OK', () => { + renderTab({ ...ADT_PT, adtBuildStatus: 'OK', adtBuildCheckedAt: '2026-05-26T10:00:00Z' }) + expect(screen.getByTestId('adt-build-health-alert')).toHaveTextContent(/available in ADT/i) + }) + + it('calls CheckADTBuild with the playtestId when Check build is clicked', async () => { + const mutate = vi.fn() + mockCheckAdtBuild.mockReturnValue({ mutate, isPending: false, isError: false, error: null }) + renderTab(ADT_PT) + const user = userEvent.setup() + await user.click(screen.getByTestId('adt-check-build-btn')) + await waitFor(() => expect(mutate).toHaveBeenCalledWith({ playtestId: 'pt-adt', data: {} })) + }) +}) diff --git a/admin/src/tabs/DistributionTab.tsx b/admin/src/tabs/DistributionTab.tsx index c3bfa62..949b717 100644 --- a/admin/src/tabs/DistributionTab.tsx +++ b/admin/src/tabs/DistributionTab.tsx @@ -1,4 +1,4 @@ -import { CheckCircleFilled, CodeSandboxOutlined } from '@ant-design/icons' +import { CheckCircleFilled, CodeSandboxOutlined, ExclamationCircleFilled } from '@ant-design/icons' import { useAppUIContext } from '@accelbyte/sdk-extend-app-ui' import { useQueryClient } from '@tanstack/react-query' import { @@ -25,6 +25,7 @@ import type { V1UploadCodesRejection } from '../playtesthubapi/generated-definit import { Key_PlaytesthubServiceAdmin, usePlaytesthubServiceAdminApi_CreateAdtBuildChange_ByPlaytestIdMutation, + usePlaytesthubServiceAdminApi_CreateAdtBuildCheck_ByPlaytestIdMutation, usePlaytesthubServiceAdminApi_CreateCodesSyncFromAg_ByPlaytestIdMutation, usePlaytesthubServiceAdminApi_CreateCodesTopUp_ByPlaytestIdMutation, usePlaytesthubServiceAdminApi_CreateCodesUpload_ByPlaytestIdMutation, @@ -93,6 +94,15 @@ function ADTPanel({ playtest }: { playtest: V1Playtest }) { onError: toastError('change build') }) + const checkBuildMutation = usePlaytesthubServiceAdminApi_CreateAdtBuildCheck_ByPlaytestIdMutation(sdk, { + onSuccess: data => { + if (data.healthy) message.success('Build is available in ADT') + else message.warning('Build is no longer available in ADT') + queryClient.invalidateQueries({ queryKey: [Key_PlaytesthubServiceAdmin.Playtests] }) + }, + onError: toastError('check build') + }) + return ( setPickerOpen(true)} disabled={!linkage?.id}> - Change Build - + + + + }>
@@ -170,6 +188,7 @@ function ADTPanel({ playtest }: { playtest: V1Playtest }) {
+ } + data-testid="adt-build-health-alert" + message="This build is no longer available in ADT" + description={`Approvals will fail until you set a fallback download URL or change the build to a current one${suffix}.`} + /> + ) + } + if (status === 'OK') { + return ( + } + data-testid="adt-build-health-alert" + message={`Build is available in ADT${suffix}.`} + /> + ) + } + return null +} + function SteamKeysPanel({ playtest }: { playtest: V1Playtest }) { const { sdk } = useAppUIContext() const queryClient = useQueryClient() diff --git a/admin/swaggers/playtesthub.json b/admin/swaggers/playtesthub.json index 7f18a38..7312f7c 100644 --- a/admin/swaggers/playtesthub.json +++ b/admin/swaggers/playtesthub.json @@ -777,6 +777,57 @@ ] } }, + "/v1/admin/namespaces/{namespace}/playtests/{playtestId}/adt/build:check": { + "post": { + "summary": "Probe + persist an ADT playtest's build download health (admin)", + "description": "Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success.", + "operationId": "PlaytesthubService_CheckADTBuild", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CheckADTBuildResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "playtestId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PlaytesthubServiceCheckADTBuildBody" + } + } + ], + "tags": [ + "PlaytesthubService" + ], + "security": [ + { + "Bearer": [] + } + ] + } + }, "/v1/admin/namespaces/{namespace}/playtests/{playtestId}/announcements": { "get": { "summary": "List announcement history for a playtest (admin)", @@ -2035,6 +2086,9 @@ } } }, + "PlaytesthubServiceCheckADTBuildBody": { + "type": "object" + }, "PlaytesthubServiceCompleteADTLinkBody": { "type": "object", "properties": { @@ -2580,6 +2634,19 @@ } } }, + "v1CheckADTBuildResponse": { + "type": "object", + "properties": { + "playtest": { + "$ref": "#/definitions/v1Playtest", + "description": "The updated playtest row, carrying the refreshed adt_build_status +\nadt_build_checked_at." + }, + "healthy": { + "type": "boolean", + "description": "healthy = (adt_build_status == \"OK\") — convenience mirror so the\ncaller need not re-parse the string." + } + } + }, "v1Code": { "type": "object", "properties": { @@ -3237,6 +3304,14 @@ }, "adtFallbackDownloadUrl": { "type": "string" + }, + "adtBuildStatus": { + "type": "string", + "description": "ADT build health (M5.C, \"build gone\" surfacing). Last observed result\nof issuing a download URL for adt_build_id: \"OK\" (a URL was minted),\n\"UNAVAILABLE\" (ADT returned build-not-found — the same signal that\nfails ApproveApplicant), or unset (never checked / not an ADT playtest).\nWritten by ApproveApplicant/RetryDM opportunistically and on demand by\nCheckADTBuild. adt_build_checked_at is when the status was last observed." + }, + "adtBuildCheckedAt": { + "type": "string", + "format": "date-time" } }, "description": "Playtest is the full admin-visible entity. Field semantics: PRD §5.1." diff --git a/cmd/pth/adt.go b/cmd/pth/adt.go index a3c3691..51c7366 100644 --- a/cmd/pth/adt.go +++ b/cmd/pth/adt.go @@ -197,7 +197,7 @@ func runADTLinkageRecover(ctx context.Context, stdout, stderr io.Writer, g *Glob func runADTBuild(ctx context.Context, stdout, stderr io.Writer, g *Globals, args []string, factory playtestClientFactory) int { if len(args) == 0 { - fmt.Fprintln(stderr, "adt build: usage: pth adt build {list|change} ...") + fmt.Fprintln(stderr, "adt build: usage: pth adt build {list|change|check} ...") return exitLocalError } action, rest := args[0], args[1:] @@ -206,6 +206,8 @@ func runADTBuild(ctx context.Context, stdout, stderr io.Writer, g *Globals, args return runADTBuildList(ctx, stdout, stderr, g, rest, factory) case "change": return runADTBuildChange(ctx, stdout, stderr, g, rest, factory) + case "check": + return runADTBuildCheck(ctx, stdout, stderr, g, rest, factory) default: fmt.Fprintf(stderr, "adt build: unknown action %q\n", action) return exitLocalError @@ -280,6 +282,31 @@ func runADTBuildChange(ctx context.Context, stdout, stderr io.Writer, g *Globals }) } +func runADTBuildCheck(ctx context.Context, stdout, stderr io.Writer, g *Globals, rest []string, factory playtestClientFactory) int { + fs := flag.NewFlagSet("adt build check", flag.ContinueOnError) + fs.SetOutput(stderr) + playtestID := fs.String("playtest-id", "", "playtest_id whose build health to probe (required)") + dryRun := fs.Bool("dry-run", false, "print the request JSON and exit without dialling") + if err := fs.Parse(rest); err != nil { + return exitLocalError + } + if *playtestID == "" { + fmt.Fprintln(stderr, "adt build check: --playtest-id is required") + return exitLocalError + } + if !g.requireNamespace(stderr, "adt build check") { + return exitLocalError + } + req := &pb.CheckADTBuildRequest{ + Namespace: g.Namespace, + PlaytestId: *playtestID, + } + return invokePlaytest(ctx, stdout, stderr, g, factory, "CheckADTBuild", req, *dryRun, + func(c pb.PlaytesthubServiceClient, cctx context.Context) (proto.Message, error) { + return c.CheckADTBuild(cctx, req) + }) +} + func runADTGames(ctx context.Context, stdout, stderr io.Writer, g *Globals, args []string, factory playtestClientFactory) int { if len(args) == 0 { fmt.Fprintln(stderr, "adt games: usage: pth adt games list --linkage-id ") diff --git a/cmd/pth/adt_test.go b/cmd/pth/adt_test.go index 4aa094f..5d8d52c 100644 --- a/cmd/pth/adt_test.go +++ b/cmd/pth/adt_test.go @@ -43,6 +43,41 @@ func TestRunADTGamesList_DryRun(t *testing.T) { } } +// TestRunADTBuildCheck_DryRun pins that `pth adt build check` emits a +// CheckADTBuild request body and does not dial. +func TestRunADTBuildCheck_DryRun(t *testing.T) { + stub := &stubPlaytestClient{} + var stdout, stderr bytes.Buffer + g := &Globals{Addr: "localhost:6565", Namespace: testNamespaceDev} + code := runADT(t.Context(), &stdout, &stderr, g, []string{ + "build", "check", + "--playtest-id", "01234567-89ab-cdef-0123-456789abcdef", + "--dry-run", + }, factoryFor(stub)) + if code != exitOK { + t.Fatalf("exit=%d, want %d (stderr=%q)", code, exitOK, stderr.String()) + } + var got map[string]any + if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &got); err != nil { + t.Fatalf("stdout not JSON: %v: %q", err, stdout.String()) + } + if got["playtest_id"] != "01234567-89ab-cdef-0123-456789abcdef" { + t.Errorf("playtest_id = %v", got["playtest_id"]) + } +} + +func TestRunADTBuildCheck_RequiresPlaytestID(t *testing.T) { + var stdout, stderr bytes.Buffer + g := &Globals{Addr: "localhost:6565", Namespace: testNamespaceDev} + code := runADT(t.Context(), &stdout, &stderr, g, []string{"build", "check", "--dry-run"}, factoryFor(&stubPlaytestClient{})) + if code != exitLocalError { + t.Fatalf("exit=%d, want %d", code, exitLocalError) + } + if !strings.Contains(stderr.String(), "--playtest-id") { + t.Errorf("stderr missing --playtest-id hint: %q", stderr.String()) + } +} + func TestRunADTGamesList_RequiresLinkageID(t *testing.T) { var stdout, stderr bytes.Buffer g := &Globals{Addr: "localhost:6565", Namespace: testNamespaceDev} diff --git a/docs/STATUS_M5.md b/docs/STATUS_M5.md index 5052ae8..a757ef4 100644 --- a/docs/STATUS_M5.md +++ b/docs/STATUS_M5.md @@ -234,6 +234,8 @@ C12. `[x]` **README walkthrough (Track C cadence) + `runbooks/admin-shell-tour.m C13. `[x]` **ChangeADTBuild — repoint an ADT playtest's build post-create + remove dead per-playtest Unlink button** — bug-report follow-up: the M5.C Distribution-tab ADT panel ([`admin/src/tabs/DistributionTab.tsx`](../admin/src/tabs/DistributionTab.tsx)) shipped two handler-less stub buttons ("Change Build", "Unlink"). Per PRD the per-playtest **Unlink** is wrong (linkage is studio-scoped — unlink lives on the Playtests list page), so it is removed; **Change Build** is now a real feature. New admin RPC `ChangeADTBuild(playtestId, adtGameId, adtBuildId)` ([`pkg/service/adt.go`](../pkg/service/adt.go)) repoints the `(adtGameId, adtBuildId)` pair under the playtest's existing — and still immutable — `adtNamespace`, re-verifying the new pair against the linkage via the same `adt.Client.ListBuilds` round-trip `CreatePlaytest` runs; new audit action `playtest.adt_build_change`. Already-approved applicants keep the URL already DM'd; future approvals + `RetryDM` re-mint against the new build (URLs minted at approve time per §4.8.3). PRD §4.7 / §4.8.6 / §5.1 + [`errors.md`](errors.md) (6 rows) + [`schema.md`](schema.md) audit bullet amended. Frontend reuses the B13 `ADTBuildPickerModal` (extracted to [`admin/src/shared/adt-build-picker.tsx`](../admin/src/shared/adt-build-picker.tsx) to break a federated-element→detail→tab circular import). `pth adt build change --playtest-id --game-id --build-id` CLI + smoke dry-run probe. Tests: 6 backend cases (happy path + build-not-in-namespace + missing fields + non-ADT + not-found + linkage-missing) + 3 admin vitest cases (Unlink absent, picker opens, mutation called) + `TestServiceDescriptorMethods` extended. +C14. `[x]` **ADT build-health surfacing ("build gone" on the detail page) + ChangeADTBuild persistence bug fix** — live-investigation follow-up: an `ApproveApplicant` 400 (`errorCode=1003303402` / `ErrBuildNotFound`) was only discoverable at approve time. Confirmed against live ADT (`develop.blackbox.accelbyte.io`): the `downloadUrls` issue 404s "build not found" even though the build still appears in `ListBuilds` — so a list-membership check is **insufficient**; the faithful signal is the side-effecting `IssueDownloadURL` call. Surfacing chosen = **persist-at-approve + on-demand check** (not auto-probe-on-load). Migration 0009 adds nullable `playtest.adt_build_status` (`OK`/`UNAVAILABLE`, CHECK-enforced) + `adt_build_checked_at`. New admin RPC `CheckADTBuild(playtestId)` ([`pkg/service/adt.go`](../pkg/service/adt.go)) probes `IssueDownloadURL` (ignoring the fallback URL — a fallback masks a dead build for approval but doesn't make the build healthy), maps `ErrBuildNotFound`→`UNAVAILABLE` / success→`OK`, persists via the new repo writer `SetADTBuildHealth` (which deliberately does NOT bump `updated_at`); transient ADT errors surface as gRPC errors without clobbering the last-known status. `resolveADTDownloadURL` (approve + RetryDM) records the same health opportunistically before any fallback masks it. Admin Distribution-tab Game Build card gains a health `Alert` (error when `UNAVAILABLE` w/ last-checked + "set a fallback URL or change the build" guidance; success when `OK`) + a "Check build" button. **Bug fix bundled**: `ChangeADTBuild` routed through `PgPlaytestStore.Update`, whose SET whitelist omits `adt_game_id`/`adt_build_id` — so the build change was silently dropped at the DB (the recovery path no-oped); its happy-path test passed only because the fake store cloned the whole struct. Added dedicated writer `UpdateADTBuild` + a real-PgStore integration regression test. `pth adt build check --playtest-id` CLI + smoke dry-run probe. PRD §4.8.3 + [`errors.md`](errors.md) (4 `CheckADTBuild` rows) amended. Tests: repo integration (migration cols, `UpdateADTBuild` persist + soft-deleted refusal, `SetADTBuildHealth` round-trip + updated_at untouched), 6 service cases (Check OK/UNAVAILABLE/non-ADT/not-found/transient-no-persist + approve-time persist OK/UNAVAILABLE), 4 admin vitest cases (no alert pre-check, UNAVAILABLE alert, OK alert, Check button mutation), `TestServiceDescriptorMethods` extended. + **Track C milestone definition of done**: every Track A + Track B phase still green; admin shell uses the detail-page-with-tabs layout end-to-end; bulk announcement DM verified end-to-end in the golden-m5 e2e (recipients reach `SENT`); platforms column replacement deployed with the migration 0007 cutover runbook step documented and tested in a fresh testcontainers boot; PRD §4 / §5.1 / §5.4 / §5.7 / §5.9 + schema.md + errors.md + CHANGELOG v2.6 complete; the prototype's three admin scenarios match the live build screen-for-screen (UX sign-off check) **modulo the telemetry surface which is explicitly deferred to M6**. ### Track D — Survey discovery (post-M5.C polish) diff --git a/docs/cli.md b/docs/cli.md index 4792159..bb1c0bd 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -147,6 +147,7 @@ PRD §4.8 / [`docs/runbooks/adt-linking.md`](runbooks/adt-linking.md). All seven | `pth adt linkage unlink --id ` | `UnlinkADT` | Idempotent; best-effort `DELETE` against ADT to drop its side's flag. | | `pth adt linkage recover --adt-namespace ` | `RecoverADTLinkage` | Adopts an orphan ADT-side flag (flag present on ADT, no local row) without an OAuth round-trip. Probes ADT first; `FailedPrecondition` if no flag exists, `AlreadyExists` if a non-deleted local row already covers `(studio, adt_namespace)`. | | `pth adt build list --linkage-id --game-id ` | `ListADTBuilds` | Defense-in-depth check: `CreatePlaytest` ADT branch reuses the same path to verify the picked `adt_build_id` belongs to `(adt_namespace, adt_game_id)`. | +| `pth adt build check --playtest-id ` | `CheckADTBuild` | Probes whether the playtest's current build can still mint a download URL (same call as `ApproveApplicant`) and persists `adt_build_status`. `healthy=false` + `UNAVAILABLE` when ADT returns build-not-found; surfaces the dead build on the detail page without an approval attempt (M5.C). | | `pth adt games list --linkage-id ` | `ListADTGames` | Drives the admin build-picker top-level dropdown so operators no longer type the `adt_game_id` by hand. STATUS_M5.md B12. | | `pth adt diagnostics` | `GetADTClientDiagnostics` | Reports which `adt.Client` kind the bootapp wired (`http` vs `mem`) plus the presence (booleans only — never values) of every env var that feeds the gate (`PLUGIN_GRPC_SERVER_AUTH_ENABLED`, `ADT_BASE_URL`, `AGS_BASE_URL`, `AGS_IAM_CLIENT_ID`, `AGS_IAM_CLIENT_SECRET`). Use when `UnlinkADT` appears to soft-delete locally but ADT still reports the linkage — `mem` here means the boot gate silently fell back and ADT-side propagation is a no-op. | diff --git a/docs/errors.md b/docs/errors.md index db0f4e0..72d9bb0 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -78,6 +78,10 @@ Consolidated from prose in PRD §4.1, §4.3, §4.6, §5.1, §5.4, §5.6 for impl | `ChangeADTBuild` | no `adt_linkage` row exists for the caller's `studio_namespace` + the playtest's `adt_namespace` — PRD §4.8.1 | `FailedPrecondition` | `no ADT linkage covers this studio + adt_namespace; link the ADT namespace first` | | `ChangeADTBuild` | ADT returned 401 on the proxied `adt.Client.ListBuilds` verification (linkage flag missing on ADT side) — PRD §4.8 | `FailedPrecondition` | `adt linkage no longer exists or service token rejected, re-link required` | | `ChangeADTBuild` | `playtest_id` does not match a live `Playtest` row (not found or soft-deleted) — PRD §4.8 | `NotFound` | `playtest not found` | +| `CheckADTBuild` | playtest `distribution_model` is not `ADT` (build health only applies to ADT playtests) — M5.C "build gone" surfacing | `FailedPrecondition` | (implementation-defined; message SHOULD reference that build health only applies to ADT playtests) | +| `CheckADTBuild` | `playtest_id` does not match a live `Playtest` row (not found or soft-deleted) — M5.C | `NotFound` | `playtest not found` | +| `CheckADTBuild` | ADT returned 401 on the `IssueDownloadURL` probe (linkage flag missing on ADT side) — M5.C | `FailedPrecondition` | `adt linkage no longer exists or service token rejected, re-link required` | +| `CheckADTBuild` | ADT returned a transient 4xx/5xx on the `IssueDownloadURL` probe — M5.C. NB: `errorCode=1003303402` ("build not found") is NOT an error here — it is a **successful** response with `healthy=false` and persisted `adt_build_status='UNAVAILABLE'` | `Unavailable` | (implementation-defined; message SHOULD reference ADT unavailability) | | `ApproveApplicant` (ADT) | ADT returned 401 on `IssueDownloadURL` (linkage flag missing on ADT side, or `union_namespace` claim shift) — PRD §4.8.3 | `FailedPrecondition` | `adt linkage no longer exists or service token rejected, re-link required` | | `ApproveApplicant` (ADT) / `GetADTDownloadInfo` | ADT returned `errorCode=1003303402` ("build not found") on `IssueDownloadURL` (linkage intact; the build was deleted from ADT) AND playtest has no `adt_fallback_download_url` set — `adt.ErrBuildNotFound`; PRD §4.8.3 / [`STATUS_M5.md`](STATUS_M5.md) | `FailedPrecondition` | `ADT build no longer exists; it may have been deleted from ADT. Set a fallback download URL or re-create the playtest with a current build.` | | `ApproveApplicant` (ADT) | ADT returned 4xx/5xx on `IssueDownloadURL` (linkage row still present) AND playtest has no `adt_fallback_download_url` set — PRD §4.8.3 | `Unavailable` | (implementation-defined; message SHOULD reference ADT unavailability and the option to set a fallback URL) | diff --git a/gateway/apidocs/api.swagger.json b/gateway/apidocs/api.swagger.json index 7f18a38..7312f7c 100644 --- a/gateway/apidocs/api.swagger.json +++ b/gateway/apidocs/api.swagger.json @@ -777,6 +777,57 @@ ] } }, + "/v1/admin/namespaces/{namespace}/playtests/{playtestId}/adt/build:check": { + "post": { + "summary": "Probe + persist an ADT playtest's build download health (admin)", + "description": "Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success.", + "operationId": "PlaytesthubService_CheckADTBuild", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CheckADTBuildResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "playtestId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PlaytesthubServiceCheckADTBuildBody" + } + } + ], + "tags": [ + "PlaytesthubService" + ], + "security": [ + { + "Bearer": [] + } + ] + } + }, "/v1/admin/namespaces/{namespace}/playtests/{playtestId}/announcements": { "get": { "summary": "List announcement history for a playtest (admin)", @@ -2035,6 +2086,9 @@ } } }, + "PlaytesthubServiceCheckADTBuildBody": { + "type": "object" + }, "PlaytesthubServiceCompleteADTLinkBody": { "type": "object", "properties": { @@ -2580,6 +2634,19 @@ } } }, + "v1CheckADTBuildResponse": { + "type": "object", + "properties": { + "playtest": { + "$ref": "#/definitions/v1Playtest", + "description": "The updated playtest row, carrying the refreshed adt_build_status +\nadt_build_checked_at." + }, + "healthy": { + "type": "boolean", + "description": "healthy = (adt_build_status == \"OK\") — convenience mirror so the\ncaller need not re-parse the string." + } + } + }, "v1Code": { "type": "object", "properties": { @@ -3237,6 +3304,14 @@ }, "adtFallbackDownloadUrl": { "type": "string" + }, + "adtBuildStatus": { + "type": "string", + "description": "ADT build health (M5.C, \"build gone\" surfacing). Last observed result\nof issuing a download URL for adt_build_id: \"OK\" (a URL was minted),\n\"UNAVAILABLE\" (ADT returned build-not-found — the same signal that\nfails ApproveApplicant), or unset (never checked / not an ADT playtest).\nWritten by ApproveApplicant/RetryDM opportunistically and on demand by\nCheckADTBuild. adt_build_checked_at is when the status was last observed." + }, + "adtBuildCheckedAt": { + "type": "string", + "format": "date-time" } }, "description": "Playtest is the full admin-visible entity. Field semantics: PRD §5.1." diff --git a/migrations/0009_m5c_adt_build_health.down.sql b/migrations/0009_m5c_adt_build_health.down.sql new file mode 100644 index 0000000..b704c8c --- /dev/null +++ b/migrations/0009_m5c_adt_build_health.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE playtest + DROP CONSTRAINT IF EXISTS playtest_adt_build_status_enum, + DROP COLUMN IF EXISTS adt_build_checked_at, + DROP COLUMN IF EXISTS adt_build_status; diff --git a/migrations/0009_m5c_adt_build_health.up.sql b/migrations/0009_m5c_adt_build_health.up.sql new file mode 100644 index 0000000..8fb3b82 --- /dev/null +++ b/migrations/0009_m5c_adt_build_health.up.sql @@ -0,0 +1,16 @@ +-- M5.C ADT build-health surfacing — make a gone/undownloadable ADT build +-- visible on the playtest detail page instead of only at ApproveApplicant +-- time. Two nullable columns record the last observed health of the +-- playtest's ADT build: +-- adt_build_status — NULL (never checked) | 'OK' | 'UNAVAILABLE'. +-- 'UNAVAILABLE' = ADT returned build-not-found on +-- the downloadUrls issue call (the same signal that +-- fails ApproveApplicant); 'OK' = a URL was minted. +-- adt_build_checked_at — when the status above was last observed. +-- Written opportunistically by ApproveApplicant / RetryDM (resolveADTDownloadURL) +-- and on demand by CheckADTBuild. Non-ADT playtests leave both NULL. +ALTER TABLE playtest + ADD COLUMN adt_build_status TEXT, + ADD COLUMN adt_build_checked_at TIMESTAMPTZ, + ADD CONSTRAINT playtest_adt_build_status_enum + CHECK (adt_build_status IS NULL OR adt_build_status IN ('OK', 'UNAVAILABLE')); diff --git a/pkg/pb/playtesthub/v1/playtesthub.pb.go b/pkg/pb/playtesthub/v1/playtesthub.pb.go index bf65ea0..c2796f1 100644 --- a/pkg/pb/playtesthub/v1/playtesthub.pb.go +++ b/pkg/pb/playtesthub/v1/playtesthub.pb.go @@ -549,8 +549,16 @@ type Playtest struct { AdtGameId *string `protobuf:"bytes,25,opt,name=adt_game_id,json=adtGameId,proto3,oneof" json:"adt_game_id,omitempty"` AdtBuildId *string `protobuf:"bytes,26,opt,name=adt_build_id,json=adtBuildId,proto3,oneof" json:"adt_build_id,omitempty"` AdtFallbackDownloadUrl *string `protobuf:"bytes,27,opt,name=adt_fallback_download_url,json=adtFallbackDownloadUrl,proto3,oneof" json:"adt_fallback_download_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // ADT build health (M5.C, "build gone" surfacing). Last observed result + // of issuing a download URL for adt_build_id: "OK" (a URL was minted), + // "UNAVAILABLE" (ADT returned build-not-found — the same signal that + // fails ApproveApplicant), or unset (never checked / not an ADT playtest). + // Written by ApproveApplicant/RetryDM opportunistically and on demand by + // CheckADTBuild. adt_build_checked_at is when the status was last observed. + AdtBuildStatus *string `protobuf:"bytes,28,opt,name=adt_build_status,json=adtBuildStatus,proto3,oneof" json:"adt_build_status,omitempty"` + AdtBuildCheckedAt *timestamppb.Timestamp `protobuf:"bytes,29,opt,name=adt_build_checked_at,json=adtBuildCheckedAt,proto3,oneof" json:"adt_build_checked_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Playtest) Reset() { @@ -772,6 +780,20 @@ func (x *Playtest) GetAdtFallbackDownloadUrl() string { return "" } +func (x *Playtest) GetAdtBuildStatus() string { + if x != nil && x.AdtBuildStatus != nil { + return *x.AdtBuildStatus + } + return "" +} + +func (x *Playtest) GetAdtBuildCheckedAt() *timestamppb.Timestamp { + if x != nil { + return x.AdtBuildCheckedAt + } + return nil +} + // PublicPlaytest is the restricted field set exposed to unauthenticated // callers on GetPublicPlaytest (PRD §5.2). type PublicPlaytest struct { @@ -6756,6 +6778,114 @@ func (x *ChangeADTBuildResponse) GetPlaytest() *Playtest { return nil } +type CheckADTBuildRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + PlaytestId string `protobuf:"bytes,2,opt,name=playtest_id,json=playtestId,proto3" json:"playtest_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckADTBuildRequest) Reset() { + *x = CheckADTBuildRequest{} + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckADTBuildRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckADTBuildRequest) ProtoMessage() {} + +func (x *CheckADTBuildRequest) ProtoReflect() protoreflect.Message { + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[99] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckADTBuildRequest.ProtoReflect.Descriptor instead. +func (*CheckADTBuildRequest) Descriptor() ([]byte, []int) { + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{99} +} + +func (x *CheckADTBuildRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CheckADTBuildRequest) GetPlaytestId() string { + if x != nil { + return x.PlaytestId + } + return "" +} + +type CheckADTBuildResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The updated playtest row, carrying the refreshed adt_build_status + + // adt_build_checked_at. + Playtest *Playtest `protobuf:"bytes,1,opt,name=playtest,proto3" json:"playtest,omitempty"` + // healthy = (adt_build_status == "OK") — convenience mirror so the + // caller need not re-parse the string. + Healthy bool `protobuf:"varint,2,opt,name=healthy,proto3" json:"healthy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckADTBuildResponse) Reset() { + *x = CheckADTBuildResponse{} + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckADTBuildResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckADTBuildResponse) ProtoMessage() {} + +func (x *CheckADTBuildResponse) ProtoReflect() protoreflect.Message { + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[100] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckADTBuildResponse.ProtoReflect.Descriptor instead. +func (*CheckADTBuildResponse) Descriptor() ([]byte, []int) { + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{100} +} + +func (x *CheckADTBuildResponse) GetPlaytest() *Playtest { + if x != nil { + return x.Playtest + } + return nil +} + +func (x *CheckADTBuildResponse) GetHealthy() bool { + if x != nil { + return x.Healthy + } + return false +} + type GetADTClientDiagnosticsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -6765,7 +6895,7 @@ type GetADTClientDiagnosticsRequest struct { func (x *GetADTClientDiagnosticsRequest) Reset() { *x = GetADTClientDiagnosticsRequest{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[99] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6777,7 +6907,7 @@ func (x *GetADTClientDiagnosticsRequest) String() string { func (*GetADTClientDiagnosticsRequest) ProtoMessage() {} func (x *GetADTClientDiagnosticsRequest) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[99] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6790,7 +6920,7 @@ func (x *GetADTClientDiagnosticsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetADTClientDiagnosticsRequest.ProtoReflect.Descriptor instead. func (*GetADTClientDiagnosticsRequest) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{99} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{101} } func (x *GetADTClientDiagnosticsRequest) GetNamespace() string { @@ -6826,7 +6956,7 @@ type GetADTClientDiagnosticsResponse struct { func (x *GetADTClientDiagnosticsResponse) Reset() { *x = GetADTClientDiagnosticsResponse{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[100] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6838,7 +6968,7 @@ func (x *GetADTClientDiagnosticsResponse) String() string { func (*GetADTClientDiagnosticsResponse) ProtoMessage() {} func (x *GetADTClientDiagnosticsResponse) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[100] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6851,7 +6981,7 @@ func (x *GetADTClientDiagnosticsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetADTClientDiagnosticsResponse.ProtoReflect.Descriptor instead. func (*GetADTClientDiagnosticsResponse) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{100} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{102} } func (x *GetADTClientDiagnosticsResponse) GetAdtClientKind() string { @@ -6924,7 +7054,7 @@ type ParticipantRow struct { func (x *ParticipantRow) Reset() { *x = ParticipantRow{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[101] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6936,7 +7066,7 @@ func (x *ParticipantRow) String() string { func (*ParticipantRow) ProtoMessage() {} func (x *ParticipantRow) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[101] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6949,7 +7079,7 @@ func (x *ParticipantRow) ProtoReflect() protoreflect.Message { // Deprecated: Use ParticipantRow.ProtoReflect.Descriptor instead. func (*ParticipantRow) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{101} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{103} } func (x *ParticipantRow) GetApplicantId() string { @@ -7048,7 +7178,7 @@ type GetPlaytestParticipantsRequest struct { func (x *GetPlaytestParticipantsRequest) Reset() { *x = GetPlaytestParticipantsRequest{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[102] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7060,7 +7190,7 @@ func (x *GetPlaytestParticipantsRequest) String() string { func (*GetPlaytestParticipantsRequest) ProtoMessage() {} func (x *GetPlaytestParticipantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[102] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7073,7 +7203,7 @@ func (x *GetPlaytestParticipantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPlaytestParticipantsRequest.ProtoReflect.Descriptor instead. func (*GetPlaytestParticipantsRequest) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{102} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{104} } func (x *GetPlaytestParticipantsRequest) GetNamespace() string { @@ -7106,7 +7236,7 @@ type GetPlaytestParticipantsResponse struct { func (x *GetPlaytestParticipantsResponse) Reset() { *x = GetPlaytestParticipantsResponse{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[103] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7118,7 +7248,7 @@ func (x *GetPlaytestParticipantsResponse) String() string { func (*GetPlaytestParticipantsResponse) ProtoMessage() {} func (x *GetPlaytestParticipantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[103] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7131,7 +7261,7 @@ func (x *GetPlaytestParticipantsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPlaytestParticipantsResponse.ProtoReflect.Descriptor instead. func (*GetPlaytestParticipantsResponse) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{103} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{105} } func (x *GetPlaytestParticipantsResponse) GetParticipants() []*ParticipantRow { @@ -7161,7 +7291,7 @@ type Announcement struct { func (x *Announcement) Reset() { *x = Announcement{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[104] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7173,7 +7303,7 @@ func (x *Announcement) String() string { func (*Announcement) ProtoMessage() {} func (x *Announcement) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[104] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7186,7 +7316,7 @@ func (x *Announcement) ProtoReflect() protoreflect.Message { // Deprecated: Use Announcement.ProtoReflect.Descriptor instead. func (*Announcement) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{104} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{106} } func (x *Announcement) GetId() string { @@ -7272,7 +7402,7 @@ type CreateAnnouncementRequest struct { func (x *CreateAnnouncementRequest) Reset() { *x = CreateAnnouncementRequest{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[105] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7284,7 +7414,7 @@ func (x *CreateAnnouncementRequest) String() string { func (*CreateAnnouncementRequest) ProtoMessage() {} func (x *CreateAnnouncementRequest) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[105] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7297,7 +7427,7 @@ func (x *CreateAnnouncementRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAnnouncementRequest.ProtoReflect.Descriptor instead. func (*CreateAnnouncementRequest) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{105} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{107} } func (x *CreateAnnouncementRequest) GetNamespace() string { @@ -7344,7 +7474,7 @@ type CreateAnnouncementResponse struct { func (x *CreateAnnouncementResponse) Reset() { *x = CreateAnnouncementResponse{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[106] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7356,7 +7486,7 @@ func (x *CreateAnnouncementResponse) String() string { func (*CreateAnnouncementResponse) ProtoMessage() {} func (x *CreateAnnouncementResponse) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[106] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7369,7 +7499,7 @@ func (x *CreateAnnouncementResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAnnouncementResponse.ProtoReflect.Descriptor instead. func (*CreateAnnouncementResponse) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{106} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{108} } func (x *CreateAnnouncementResponse) GetAnnouncement() *Announcement { @@ -7389,7 +7519,7 @@ type ListAnnouncementsRequest struct { func (x *ListAnnouncementsRequest) Reset() { *x = ListAnnouncementsRequest{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[107] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7401,7 +7531,7 @@ func (x *ListAnnouncementsRequest) String() string { func (*ListAnnouncementsRequest) ProtoMessage() {} func (x *ListAnnouncementsRequest) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[107] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7414,7 +7544,7 @@ func (x *ListAnnouncementsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAnnouncementsRequest.ProtoReflect.Descriptor instead. func (*ListAnnouncementsRequest) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{107} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{109} } func (x *ListAnnouncementsRequest) GetNamespace() string { @@ -7440,7 +7570,7 @@ type ListAnnouncementsResponse struct { func (x *ListAnnouncementsResponse) Reset() { *x = ListAnnouncementsResponse{} - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[108] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7452,7 +7582,7 @@ func (x *ListAnnouncementsResponse) String() string { func (*ListAnnouncementsResponse) ProtoMessage() {} func (x *ListAnnouncementsResponse) ProtoReflect() protoreflect.Message { - mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[108] + mi := &file_playtesthub_v1_playtesthub_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7465,7 +7595,7 @@ func (x *ListAnnouncementsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAnnouncementsResponse.ProtoReflect.Descriptor instead. func (*ListAnnouncementsResponse) Descriptor() ([]byte, []int) { - return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{108} + return file_playtesthub_v1_playtesthub_proto_rawDescGZIP(), []int{110} } func (x *ListAnnouncementsResponse) GetAnnouncements() []*Announcement { @@ -7479,8 +7609,7 @@ var File_playtesthub_v1_playtesthub_proto protoreflect.FileDescriptor const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + "\n" + - " playtesthub/v1/playtesthub.proto\x12\x0eplaytesthub.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fplaytesthub/v1/permission.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xf3\n" + - "\n" + + " playtesthub/v1/playtesthub.proto\x12\x0eplaytesthub.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fplaytesthub/v1/permission.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xa2\f\n" + "\bPlaytest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1c\n" + "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12\x12\n" + @@ -7513,7 +7642,10 @@ const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + "\vadt_game_id\x18\x19 \x01(\tH\x06R\tadtGameId\x88\x01\x01\x12%\n" + "\fadt_build_id\x18\x1a \x01(\tH\aR\n" + "adtBuildId\x88\x01\x01\x12>\n" + - "\x19adt_fallback_download_url\x18\x1b \x01(\tH\bR\x16adtFallbackDownloadUrl\x88\x01\x01B\f\n" + + "\x19adt_fallback_download_url\x18\x1b \x01(\tH\bR\x16adtFallbackDownloadUrl\x88\x01\x01\x12-\n" + + "\x10adt_build_status\x18\x1c \x01(\tH\tR\x0eadtBuildStatus\x88\x01\x01\x12P\n" + + "\x14adt_build_checked_at\x18\x1d \x01(\v2\x1a.google.protobuf.TimestampH\n" + + "R\x11adtBuildCheckedAt\x88\x01\x01B\f\n" + "\n" + "_survey_idB\x0e\n" + "\f_ags_item_idB\x12\n" + @@ -7523,7 +7655,9 @@ const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + "\x0e_adt_namespaceB\x0e\n" + "\f_adt_game_idB\x0f\n" + "\r_adt_build_idB\x1c\n" + - "\x1a_adt_fallback_download_url\"\xac\x02\n" + + "\x1a_adt_fallback_download_urlB\x13\n" + + "\x11_adt_build_statusB\x17\n" + + "\x15_adt_build_checked_at\"\xac\x02\n" + "\x0ePublicPlaytest\x12\x12\n" + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + @@ -7984,7 +8118,14 @@ const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + "\fadt_build_id\x18\x04 \x01(\tR\n" + "adtBuildId\"N\n" + "\x16ChangeADTBuildResponse\x124\n" + - "\bplaytest\x18\x01 \x01(\v2\x18.playtesthub.v1.PlaytestR\bplaytest\">\n" + + "\bplaytest\x18\x01 \x01(\v2\x18.playtesthub.v1.PlaytestR\bplaytest\"U\n" + + "\x14CheckADTBuildRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\vplaytest_id\x18\x02 \x01(\tR\n" + + "playtestId\"g\n" + + "\x15CheckADTBuildResponse\x124\n" + + "\bplaytest\x18\x01 \x01(\v2\x18.playtesthub.v1.PlaytestR\bplaytest\x12\x18\n" + + "\ahealthy\x18\x02 \x01(\bR\ahealthy\">\n" + "\x1eGetADTClientDiagnosticsRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\"\xaa\x02\n" + "\x1fGetADTClientDiagnosticsResponse\x12&\n" + @@ -8093,7 +8234,7 @@ const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + " SURVEY_QUESTION_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n" + "\x19SURVEY_QUESTION_TYPE_TEXT\x10\x01\x12\x1f\n" + "\x1bSURVEY_QUESTION_TYPE_RATING\x10\x02\x12%\n" + - "!SURVEY_QUESTION_TYPE_MULTI_CHOICE\x10\x032ݣ\x01\n" + + "!SURVEY_QUESTION_TYPE_MULTI_CHOICE\x10\x032\xa6\xa9\x01\n" + "\x12PlaytesthubService\x12\xa0\x02\n" + "\x11GetPublicPlaytest\x12(.playtesthub.v1.GetPublicPlaytestRequest\x1a).playtesthub.v1.GetPublicPlaytestResponse\"\xb5\x01\x92A\x8e\x01\x12(Get a playtest by slug (unauthenticated)\x1abReturns the public field subset for an OPEN playtest. NotFound for DRAFT, CLOSED, or soft-deleted.\x82\xd3\xe4\x93\x02\x1d\x12\x1b/v1/public/playtests/{slug}\x12\xa7\x03\n" + "\x0fGetPublicConfig\x12&.playtesthub.v1.GetPublicConfigRequest\x1a'.playtesthub.v1.GetPublicConfigResponse\"\xc2\x02\x92A\xa5\x02\x12.Get the public client config (unauthenticated)\x1a\xf2\x01Returns environment-derived client config that both the admin and player frontends need to construct cross-app URLs. player_base_url is the public origin of the player Svelte bundle (from backend env PLAYER_BASE_URL); empty string when unset.\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/public/config\x12\xac\x02\n" + @@ -8251,7 +8392,11 @@ const file_playtesthub_v1_playtesthub_proto_rawDesc = "" + "\x0eChangeADTBuild\x12%.playtesthub.v1.ChangeADTBuildRequest\x1a&.playtesthub.v1.ChangeADTBuildResponse\"\xee\x04\x92A\xe6\x03\x124Repoint an ADT playtest at a different build (admin)\x1a\x9f\x03Mutates adt_game_id + adt_build_id on an ADT playtest after verifying the pair against the linked ADT namespace via ListBuilds. adt_namespace is immutable (relink instead). Non-ADT playtest → FailedPrecondition. Build absent from the (namespace, game) pair → InvalidArgument. Already-approved applicants keep the download URL already DM'd; future approvals + RetryDM re-mint against the new build (PRD §4.8.3).b\f\n" + "\n" + "\n" + - "\x06Bearer\x12\x00\x8a\xb5\x18(ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI\x90\xb5\x18\x04\x82\xd3\xe4\x93\x02N:\x01*\"I/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:change\x12\xca\x04\n" + + "\x06Bearer\x12\x00\x8a\xb5\x18(ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI\x90\xb5\x18\x04\x82\xd3\xe4\x93\x02N:\x01*\"I/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:change\x12\xc6\x05\n" + + "\rCheckADTBuild\x12$.playtesthub.v1.CheckADTBuildRequest\x1a%.playtesthub.v1.CheckADTBuildResponse\"\xe7\x04\x92A\xe0\x03\x12?Probe + persist an ADT playtest's build download health (admin)\x1a\x8e\x03Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success.b\f\n" + + "\n" + + "\n" + + "\x06Bearer\x12\x00\x8a\xb5\x18(ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI\x90\xb5\x18\x04\x82\xd3\xe4\x93\x02M:\x01*\"H/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:check\x12\xca\x04\n" + "\x17GetPlaytestParticipants\x12..playtesthub.v1.GetPlaytestParticipantsRequest\x1a/.playtesthub.v1.GetPlaytestParticipantsResponse\"\xcd\x03\x92A\xcc\x02\x12MList participants with derived code-sent timestamps (admin; M5.C detail page)\x1a\xec\x01Read joins applicant + the latest dm.sent audit row to derive code_sent_at for STEAM_KEYS / AGS_CAMPAIGN rows; ADT rows return NULL code_sent_at. Four ADT telemetry cache fields ship in the response shape but stay NULL/zero across M5.C.b\f\n" + "\n" + "\n" + @@ -8281,7 +8426,7 @@ func file_playtesthub_v1_playtesthub_proto_rawDescGZIP() []byte { } var file_playtesthub_v1_playtesthub_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_playtesthub_v1_playtesthub_proto_msgTypes = make([]protoimpl.MessageInfo, 109) +var file_playtesthub_v1_playtesthub_proto_msgTypes = make([]protoimpl.MessageInfo, 111) var file_playtesthub_v1_playtesthub_proto_goTypes = []any{ (Platform)(0), // 0: playtesthub.v1.Platform (PlaytestStatus)(0), // 1: playtesthub.v1.PlaytestStatus @@ -8391,215 +8536,221 @@ var file_playtesthub_v1_playtesthub_proto_goTypes = []any{ (*ListADTGamesResponse)(nil), // 105: playtesthub.v1.ListADTGamesResponse (*ChangeADTBuildRequest)(nil), // 106: playtesthub.v1.ChangeADTBuildRequest (*ChangeADTBuildResponse)(nil), // 107: playtesthub.v1.ChangeADTBuildResponse - (*GetADTClientDiagnosticsRequest)(nil), // 108: playtesthub.v1.GetADTClientDiagnosticsRequest - (*GetADTClientDiagnosticsResponse)(nil), // 109: playtesthub.v1.GetADTClientDiagnosticsResponse - (*ParticipantRow)(nil), // 110: playtesthub.v1.ParticipantRow - (*GetPlaytestParticipantsRequest)(nil), // 111: playtesthub.v1.GetPlaytestParticipantsRequest - (*GetPlaytestParticipantsResponse)(nil), // 112: playtesthub.v1.GetPlaytestParticipantsResponse - (*Announcement)(nil), // 113: playtesthub.v1.Announcement - (*CreateAnnouncementRequest)(nil), // 114: playtesthub.v1.CreateAnnouncementRequest - (*CreateAnnouncementResponse)(nil), // 115: playtesthub.v1.CreateAnnouncementResponse - (*ListAnnouncementsRequest)(nil), // 116: playtesthub.v1.ListAnnouncementsRequest - (*ListAnnouncementsResponse)(nil), // 117: playtesthub.v1.ListAnnouncementsResponse - (*timestamppb.Timestamp)(nil), // 118: google.protobuf.Timestamp + (*CheckADTBuildRequest)(nil), // 108: playtesthub.v1.CheckADTBuildRequest + (*CheckADTBuildResponse)(nil), // 109: playtesthub.v1.CheckADTBuildResponse + (*GetADTClientDiagnosticsRequest)(nil), // 110: playtesthub.v1.GetADTClientDiagnosticsRequest + (*GetADTClientDiagnosticsResponse)(nil), // 111: playtesthub.v1.GetADTClientDiagnosticsResponse + (*ParticipantRow)(nil), // 112: playtesthub.v1.ParticipantRow + (*GetPlaytestParticipantsRequest)(nil), // 113: playtesthub.v1.GetPlaytestParticipantsRequest + (*GetPlaytestParticipantsResponse)(nil), // 114: playtesthub.v1.GetPlaytestParticipantsResponse + (*Announcement)(nil), // 115: playtesthub.v1.Announcement + (*CreateAnnouncementRequest)(nil), // 116: playtesthub.v1.CreateAnnouncementRequest + (*CreateAnnouncementResponse)(nil), // 117: playtesthub.v1.CreateAnnouncementResponse + (*ListAnnouncementsRequest)(nil), // 118: playtesthub.v1.ListAnnouncementsRequest + (*ListAnnouncementsResponse)(nil), // 119: playtesthub.v1.ListAnnouncementsResponse + (*timestamppb.Timestamp)(nil), // 120: google.protobuf.Timestamp } var file_playtesthub_v1_playtesthub_proto_depIdxs = []int32{ 0, // 0: playtesthub.v1.Playtest.platforms:type_name -> playtesthub.v1.Platform - 118, // 1: playtesthub.v1.Playtest.starts_at:type_name -> google.protobuf.Timestamp - 118, // 2: playtesthub.v1.Playtest.ends_at:type_name -> google.protobuf.Timestamp + 120, // 1: playtesthub.v1.Playtest.starts_at:type_name -> google.protobuf.Timestamp + 120, // 2: playtesthub.v1.Playtest.ends_at:type_name -> google.protobuf.Timestamp 1, // 3: playtesthub.v1.Playtest.status:type_name -> playtesthub.v1.PlaytestStatus 2, // 4: playtesthub.v1.Playtest.distribution_model:type_name -> playtesthub.v1.DistributionModel - 118, // 5: playtesthub.v1.Playtest.created_at:type_name -> google.protobuf.Timestamp - 118, // 6: playtesthub.v1.Playtest.updated_at:type_name -> google.protobuf.Timestamp - 118, // 7: playtesthub.v1.Playtest.deleted_at:type_name -> google.protobuf.Timestamp - 0, // 8: playtesthub.v1.PublicPlaytest.platforms:type_name -> playtesthub.v1.Platform - 118, // 9: playtesthub.v1.PublicPlaytest.starts_at:type_name -> google.protobuf.Timestamp - 118, // 10: playtesthub.v1.PublicPlaytest.ends_at:type_name -> google.protobuf.Timestamp - 0, // 11: playtesthub.v1.PlayerPlaytest.platforms:type_name -> playtesthub.v1.Platform - 118, // 12: playtesthub.v1.PlayerPlaytest.starts_at:type_name -> google.protobuf.Timestamp - 118, // 13: playtesthub.v1.PlayerPlaytest.ends_at:type_name -> google.protobuf.Timestamp - 1, // 14: playtesthub.v1.PlayerPlaytest.status:type_name -> playtesthub.v1.PlaytestStatus - 2, // 15: playtesthub.v1.PlayerPlaytest.distribution_model:type_name -> playtesthub.v1.DistributionModel - 0, // 16: playtesthub.v1.Applicant.platforms:type_name -> playtesthub.v1.Platform - 3, // 17: playtesthub.v1.Applicant.status:type_name -> playtesthub.v1.ApplicantStatus - 118, // 18: playtesthub.v1.Applicant.approved_at:type_name -> google.protobuf.Timestamp - 4, // 19: playtesthub.v1.Applicant.last_dm_status:type_name -> playtesthub.v1.DmStatus - 118, // 20: playtesthub.v1.Applicant.last_dm_attempt_at:type_name -> google.protobuf.Timestamp - 118, // 21: playtesthub.v1.Applicant.created_at:type_name -> google.protobuf.Timestamp - 118, // 22: playtesthub.v1.Applicant.survey_response_submitted_at:type_name -> google.protobuf.Timestamp - 118, // 23: playtesthub.v1.NDAAcceptance.accepted_at:type_name -> google.protobuf.Timestamp - 5, // 24: playtesthub.v1.Code.state:type_name -> playtesthub.v1.CodeState - 118, // 25: playtesthub.v1.Code.reserved_at:type_name -> google.protobuf.Timestamp - 118, // 26: playtesthub.v1.Code.granted_at:type_name -> google.protobuf.Timestamp - 118, // 27: playtesthub.v1.Code.created_at:type_name -> google.protobuf.Timestamp - 8, // 28: playtesthub.v1.SurveyQuestion.type:type_name -> playtesthub.v1.SurveyQuestionType - 16, // 29: playtesthub.v1.SurveyQuestion.options:type_name -> playtesthub.v1.MultiChoiceOption - 17, // 30: playtesthub.v1.Survey.questions:type_name -> playtesthub.v1.SurveyQuestion - 118, // 31: playtesthub.v1.Survey.created_at:type_name -> google.protobuf.Timestamp - 19, // 32: playtesthub.v1.SurveyAnswer.multi_choice:type_name -> playtesthub.v1.SurveyMultiChoiceAnswer - 20, // 33: playtesthub.v1.SurveyResponse.answers:type_name -> playtesthub.v1.SurveyAnswer - 118, // 34: playtesthub.v1.SurveyResponse.submitted_at:type_name -> google.protobuf.Timestamp - 118, // 35: playtesthub.v1.AuditLogEntry.created_at:type_name -> google.protobuf.Timestamp - 10, // 36: playtesthub.v1.GetPublicPlaytestResponse.playtest:type_name -> playtesthub.v1.PublicPlaytest - 11, // 37: playtesthub.v1.GetPlaytestForPlayerResponse.playtest:type_name -> playtesthub.v1.PlayerPlaytest - 9, // 38: playtesthub.v1.AdminGetPlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest - 9, // 39: playtesthub.v1.ListPlaytestsResponse.playtests:type_name -> playtesthub.v1.Playtest - 0, // 40: playtesthub.v1.CreatePlaytestRequest.platforms:type_name -> playtesthub.v1.Platform - 118, // 41: playtesthub.v1.CreatePlaytestRequest.starts_at:type_name -> google.protobuf.Timestamp - 118, // 42: playtesthub.v1.CreatePlaytestRequest.ends_at:type_name -> google.protobuf.Timestamp - 2, // 43: playtesthub.v1.CreatePlaytestRequest.distribution_model:type_name -> playtesthub.v1.DistributionModel - 9, // 44: playtesthub.v1.CreatePlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest - 0, // 45: playtesthub.v1.EditPlaytestRequest.platforms:type_name -> playtesthub.v1.Platform - 118, // 46: playtesthub.v1.EditPlaytestRequest.starts_at:type_name -> google.protobuf.Timestamp - 118, // 47: playtesthub.v1.EditPlaytestRequest.ends_at:type_name -> google.protobuf.Timestamp - 9, // 48: playtesthub.v1.EditPlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest - 1, // 49: playtesthub.v1.TransitionPlaytestStatusRequest.target_status:type_name -> playtesthub.v1.PlaytestStatus - 9, // 50: playtesthub.v1.TransitionPlaytestStatusResponse.playtest:type_name -> playtesthub.v1.Playtest - 0, // 51: playtesthub.v1.SignupRequest.platforms:type_name -> playtesthub.v1.Platform - 12, // 52: playtesthub.v1.SignupResponse.applicant:type_name -> playtesthub.v1.Applicant - 12, // 53: playtesthub.v1.GetApplicantStatusResponse.applicant:type_name -> playtesthub.v1.Applicant - 13, // 54: playtesthub.v1.AcceptNDAResponse.acceptance:type_name -> playtesthub.v1.NDAAcceptance - 2, // 55: playtesthub.v1.GetGrantedCodeResponse.distribution_model:type_name -> playtesthub.v1.DistributionModel - 118, // 56: playtesthub.v1.GetADTDownloadInfoResponse.expires_at:type_name -> google.protobuf.Timestamp - 3, // 57: playtesthub.v1.ListApplicantsRequest.status_filter:type_name -> playtesthub.v1.ApplicantStatus - 12, // 58: playtesthub.v1.ListApplicantsResponse.applicants:type_name -> playtesthub.v1.Applicant - 12, // 59: playtesthub.v1.ApproveApplicantResponse.applicant:type_name -> playtesthub.v1.Applicant - 12, // 60: playtesthub.v1.RejectApplicantResponse.applicant:type_name -> playtesthub.v1.Applicant - 12, // 61: playtesthub.v1.RetryDMResponse.applicant:type_name -> playtesthub.v1.Applicant - 64, // 62: playtesthub.v1.UploadCodesResponse.rejections:type_name -> playtesthub.v1.UploadCodesRejection - 15, // 63: playtesthub.v1.TopUpCodesResponse.pool:type_name -> playtesthub.v1.CodePoolStats - 15, // 64: playtesthub.v1.SyncFromAGSResponse.pool:type_name -> playtesthub.v1.CodePoolStats - 15, // 65: playtesthub.v1.GetCodePoolResponse.stats:type_name -> playtesthub.v1.CodePoolStats - 14, // 66: playtesthub.v1.GetCodePoolResponse.codes:type_name -> playtesthub.v1.Code - 17, // 67: playtesthub.v1.CreateSurveyRequest.questions:type_name -> playtesthub.v1.SurveyQuestion - 18, // 68: playtesthub.v1.CreateSurveyResponse.survey:type_name -> playtesthub.v1.Survey - 17, // 69: playtesthub.v1.EditSurveyRequest.questions:type_name -> playtesthub.v1.SurveyQuestion - 18, // 70: playtesthub.v1.EditSurveyResponse.survey:type_name -> playtesthub.v1.Survey - 18, // 71: playtesthub.v1.GetSurveyResponse.survey:type_name -> playtesthub.v1.Survey - 20, // 72: playtesthub.v1.SubmitSurveyResponseRequest.answers:type_name -> playtesthub.v1.SurveyAnswer - 21, // 73: playtesthub.v1.SubmitSurveyResponseResponse.response:type_name -> playtesthub.v1.SurveyResponse - 21, // 74: playtesthub.v1.ListSurveyResponsesResponse.responses:type_name -> playtesthub.v1.SurveyResponse - 22, // 75: playtesthub.v1.ListAuditLogResponse.entries:type_name -> playtesthub.v1.AuditLogEntry - 118, // 76: playtesthub.v1.WorkerHealthEntry.last_tick_at:type_name -> google.protobuf.Timestamp - 118, // 77: playtesthub.v1.WorkerHealthEntry.expires_at:type_name -> google.protobuf.Timestamp - 87, // 78: playtesthub.v1.GetWorkerHealthResponse.workers:type_name -> playtesthub.v1.WorkerHealthEntry - 118, // 79: playtesthub.v1.ADTLinkage.linked_at:type_name -> google.protobuf.Timestamp - 118, // 80: playtesthub.v1.ADTLinkage.deleted_at:type_name -> google.protobuf.Timestamp - 118, // 81: playtesthub.v1.ADTBuild.uploaded_at:type_name -> google.protobuf.Timestamp - 89, // 82: playtesthub.v1.ListADTLinkagesResponse.linkages:type_name -> playtesthub.v1.ADTLinkage - 118, // 83: playtesthub.v1.StartADTLinkResponse.expires_at:type_name -> google.protobuf.Timestamp - 89, // 84: playtesthub.v1.CompleteADTLinkResponse.linkage:type_name -> playtesthub.v1.ADTLinkage - 89, // 85: playtesthub.v1.RecoverADTLinkageResponse.linkage:type_name -> playtesthub.v1.ADTLinkage - 90, // 86: playtesthub.v1.ListADTBuildsResponse.builds:type_name -> playtesthub.v1.ADTBuild - 118, // 87: playtesthub.v1.ADTGame.created_at:type_name -> google.protobuf.Timestamp - 103, // 88: playtesthub.v1.ListADTGamesResponse.games:type_name -> playtesthub.v1.ADTGame - 9, // 89: playtesthub.v1.ChangeADTBuildResponse.playtest:type_name -> playtesthub.v1.Playtest - 118, // 90: playtesthub.v1.ParticipantRow.signup_at:type_name -> google.protobuf.Timestamp - 118, // 91: playtesthub.v1.ParticipantRow.nda_accepted_at:type_name -> google.protobuf.Timestamp - 118, // 92: playtesthub.v1.ParticipantRow.code_sent_at:type_name -> google.protobuf.Timestamp - 3, // 93: playtesthub.v1.ParticipantRow.status:type_name -> playtesthub.v1.ApplicantStatus - 118, // 94: playtesthub.v1.ParticipantRow.adt_download_at:type_name -> google.protobuf.Timestamp - 3, // 95: playtesthub.v1.GetPlaytestParticipantsRequest.status_filter:type_name -> playtesthub.v1.ApplicantStatus - 110, // 96: playtesthub.v1.GetPlaytestParticipantsResponse.participants:type_name -> playtesthub.v1.ParticipantRow - 6, // 97: playtesthub.v1.Announcement.send_to_filter:type_name -> playtesthub.v1.AnnouncementSendToFilter - 7, // 98: playtesthub.v1.Announcement.status:type_name -> playtesthub.v1.AnnouncementStatus - 118, // 99: playtesthub.v1.Announcement.created_at:type_name -> google.protobuf.Timestamp - 6, // 100: playtesthub.v1.CreateAnnouncementRequest.send_to_filter:type_name -> playtesthub.v1.AnnouncementSendToFilter - 113, // 101: playtesthub.v1.CreateAnnouncementResponse.announcement:type_name -> playtesthub.v1.Announcement - 113, // 102: playtesthub.v1.ListAnnouncementsResponse.announcements:type_name -> playtesthub.v1.Announcement - 23, // 103: playtesthub.v1.PlaytesthubService.GetPublicPlaytest:input_type -> playtesthub.v1.GetPublicPlaytestRequest - 25, // 104: playtesthub.v1.PlaytesthubService.GetPublicConfig:input_type -> playtesthub.v1.GetPublicConfigRequest - 27, // 105: playtesthub.v1.PlaytesthubService.GetPlaytestForPlayer:input_type -> playtesthub.v1.GetPlaytestForPlayerRequest - 29, // 106: playtesthub.v1.PlaytesthubService.WhoAmI:input_type -> playtesthub.v1.WhoAmIRequest - 31, // 107: playtesthub.v1.PlaytesthubService.AdminGetPlaytest:input_type -> playtesthub.v1.AdminGetPlaytestRequest - 33, // 108: playtesthub.v1.PlaytesthubService.ListPlaytests:input_type -> playtesthub.v1.ListPlaytestsRequest - 35, // 109: playtesthub.v1.PlaytesthubService.CreatePlaytest:input_type -> playtesthub.v1.CreatePlaytestRequest - 37, // 110: playtesthub.v1.PlaytesthubService.EditPlaytest:input_type -> playtesthub.v1.EditPlaytestRequest - 39, // 111: playtesthub.v1.PlaytesthubService.SoftDeletePlaytest:input_type -> playtesthub.v1.SoftDeletePlaytestRequest - 41, // 112: playtesthub.v1.PlaytesthubService.TransitionPlaytestStatus:input_type -> playtesthub.v1.TransitionPlaytestStatusRequest - 47, // 113: playtesthub.v1.PlaytesthubService.ExchangeDiscordCode:input_type -> playtesthub.v1.ExchangeDiscordCodeRequest - 43, // 114: playtesthub.v1.PlaytesthubService.Signup:input_type -> playtesthub.v1.SignupRequest - 45, // 115: playtesthub.v1.PlaytesthubService.GetApplicantStatus:input_type -> playtesthub.v1.GetApplicantStatusRequest - 49, // 116: playtesthub.v1.PlaytesthubService.AcceptNDA:input_type -> playtesthub.v1.AcceptNDARequest - 51, // 117: playtesthub.v1.PlaytesthubService.GetGrantedCode:input_type -> playtesthub.v1.GetGrantedCodeRequest - 53, // 118: playtesthub.v1.PlaytesthubService.GetADTDownloadInfo:input_type -> playtesthub.v1.GetADTDownloadInfoRequest - 55, // 119: playtesthub.v1.PlaytesthubService.ListApplicants:input_type -> playtesthub.v1.ListApplicantsRequest - 57, // 120: playtesthub.v1.PlaytesthubService.ApproveApplicant:input_type -> playtesthub.v1.ApproveApplicantRequest - 59, // 121: playtesthub.v1.PlaytesthubService.RejectApplicant:input_type -> playtesthub.v1.RejectApplicantRequest - 61, // 122: playtesthub.v1.PlaytesthubService.RetryDM:input_type -> playtesthub.v1.RetryDMRequest - 63, // 123: playtesthub.v1.PlaytesthubService.UploadCodes:input_type -> playtesthub.v1.UploadCodesRequest - 66, // 124: playtesthub.v1.PlaytesthubService.TopUpCodes:input_type -> playtesthub.v1.TopUpCodesRequest - 68, // 125: playtesthub.v1.PlaytesthubService.SyncFromAGS:input_type -> playtesthub.v1.SyncFromAGSRequest - 70, // 126: playtesthub.v1.PlaytesthubService.GetCodePool:input_type -> playtesthub.v1.GetCodePoolRequest - 72, // 127: playtesthub.v1.PlaytesthubService.CreateSurvey:input_type -> playtesthub.v1.CreateSurveyRequest - 74, // 128: playtesthub.v1.PlaytesthubService.EditSurvey:input_type -> playtesthub.v1.EditSurveyRequest - 76, // 129: playtesthub.v1.PlaytesthubService.GetSurvey:input_type -> playtesthub.v1.GetSurveyRequest - 78, // 130: playtesthub.v1.PlaytesthubService.SubmitSurveyResponse:input_type -> playtesthub.v1.SubmitSurveyResponseRequest - 80, // 131: playtesthub.v1.PlaytesthubService.ListSurveyResponses:input_type -> playtesthub.v1.ListSurveyResponsesRequest - 82, // 132: playtesthub.v1.PlaytesthubService.ListAuditLog:input_type -> playtesthub.v1.ListAuditLogRequest - 84, // 133: playtesthub.v1.PlaytesthubService.RetryFailedDms:input_type -> playtesthub.v1.RetryFailedDmsRequest - 86, // 134: playtesthub.v1.PlaytesthubService.GetWorkerHealth:input_type -> playtesthub.v1.GetWorkerHealthRequest - 91, // 135: playtesthub.v1.PlaytesthubService.ListADTLinkages:input_type -> playtesthub.v1.ListADTLinkagesRequest - 93, // 136: playtesthub.v1.PlaytesthubService.StartADTLink:input_type -> playtesthub.v1.StartADTLinkRequest - 95, // 137: playtesthub.v1.PlaytesthubService.CompleteADTLink:input_type -> playtesthub.v1.CompleteADTLinkRequest - 97, // 138: playtesthub.v1.PlaytesthubService.UnlinkADT:input_type -> playtesthub.v1.UnlinkADTRequest - 99, // 139: playtesthub.v1.PlaytesthubService.RecoverADTLinkage:input_type -> playtesthub.v1.RecoverADTLinkageRequest - 101, // 140: playtesthub.v1.PlaytesthubService.ListADTBuilds:input_type -> playtesthub.v1.ListADTBuildsRequest - 104, // 141: playtesthub.v1.PlaytesthubService.ListADTGames:input_type -> playtesthub.v1.ListADTGamesRequest - 108, // 142: playtesthub.v1.PlaytesthubService.GetADTClientDiagnostics:input_type -> playtesthub.v1.GetADTClientDiagnosticsRequest - 106, // 143: playtesthub.v1.PlaytesthubService.ChangeADTBuild:input_type -> playtesthub.v1.ChangeADTBuildRequest - 111, // 144: playtesthub.v1.PlaytesthubService.GetPlaytestParticipants:input_type -> playtesthub.v1.GetPlaytestParticipantsRequest - 114, // 145: playtesthub.v1.PlaytesthubService.CreateAnnouncement:input_type -> playtesthub.v1.CreateAnnouncementRequest - 116, // 146: playtesthub.v1.PlaytesthubService.ListAnnouncements:input_type -> playtesthub.v1.ListAnnouncementsRequest - 24, // 147: playtesthub.v1.PlaytesthubService.GetPublicPlaytest:output_type -> playtesthub.v1.GetPublicPlaytestResponse - 26, // 148: playtesthub.v1.PlaytesthubService.GetPublicConfig:output_type -> playtesthub.v1.GetPublicConfigResponse - 28, // 149: playtesthub.v1.PlaytesthubService.GetPlaytestForPlayer:output_type -> playtesthub.v1.GetPlaytestForPlayerResponse - 30, // 150: playtesthub.v1.PlaytesthubService.WhoAmI:output_type -> playtesthub.v1.WhoAmIResponse - 32, // 151: playtesthub.v1.PlaytesthubService.AdminGetPlaytest:output_type -> playtesthub.v1.AdminGetPlaytestResponse - 34, // 152: playtesthub.v1.PlaytesthubService.ListPlaytests:output_type -> playtesthub.v1.ListPlaytestsResponse - 36, // 153: playtesthub.v1.PlaytesthubService.CreatePlaytest:output_type -> playtesthub.v1.CreatePlaytestResponse - 38, // 154: playtesthub.v1.PlaytesthubService.EditPlaytest:output_type -> playtesthub.v1.EditPlaytestResponse - 40, // 155: playtesthub.v1.PlaytesthubService.SoftDeletePlaytest:output_type -> playtesthub.v1.SoftDeletePlaytestResponse - 42, // 156: playtesthub.v1.PlaytesthubService.TransitionPlaytestStatus:output_type -> playtesthub.v1.TransitionPlaytestStatusResponse - 48, // 157: playtesthub.v1.PlaytesthubService.ExchangeDiscordCode:output_type -> playtesthub.v1.ExchangeDiscordCodeResponse - 44, // 158: playtesthub.v1.PlaytesthubService.Signup:output_type -> playtesthub.v1.SignupResponse - 46, // 159: playtesthub.v1.PlaytesthubService.GetApplicantStatus:output_type -> playtesthub.v1.GetApplicantStatusResponse - 50, // 160: playtesthub.v1.PlaytesthubService.AcceptNDA:output_type -> playtesthub.v1.AcceptNDAResponse - 52, // 161: playtesthub.v1.PlaytesthubService.GetGrantedCode:output_type -> playtesthub.v1.GetGrantedCodeResponse - 54, // 162: playtesthub.v1.PlaytesthubService.GetADTDownloadInfo:output_type -> playtesthub.v1.GetADTDownloadInfoResponse - 56, // 163: playtesthub.v1.PlaytesthubService.ListApplicants:output_type -> playtesthub.v1.ListApplicantsResponse - 58, // 164: playtesthub.v1.PlaytesthubService.ApproveApplicant:output_type -> playtesthub.v1.ApproveApplicantResponse - 60, // 165: playtesthub.v1.PlaytesthubService.RejectApplicant:output_type -> playtesthub.v1.RejectApplicantResponse - 62, // 166: playtesthub.v1.PlaytesthubService.RetryDM:output_type -> playtesthub.v1.RetryDMResponse - 65, // 167: playtesthub.v1.PlaytesthubService.UploadCodes:output_type -> playtesthub.v1.UploadCodesResponse - 67, // 168: playtesthub.v1.PlaytesthubService.TopUpCodes:output_type -> playtesthub.v1.TopUpCodesResponse - 69, // 169: playtesthub.v1.PlaytesthubService.SyncFromAGS:output_type -> playtesthub.v1.SyncFromAGSResponse - 71, // 170: playtesthub.v1.PlaytesthubService.GetCodePool:output_type -> playtesthub.v1.GetCodePoolResponse - 73, // 171: playtesthub.v1.PlaytesthubService.CreateSurvey:output_type -> playtesthub.v1.CreateSurveyResponse - 75, // 172: playtesthub.v1.PlaytesthubService.EditSurvey:output_type -> playtesthub.v1.EditSurveyResponse - 77, // 173: playtesthub.v1.PlaytesthubService.GetSurvey:output_type -> playtesthub.v1.GetSurveyResponse - 79, // 174: playtesthub.v1.PlaytesthubService.SubmitSurveyResponse:output_type -> playtesthub.v1.SubmitSurveyResponseResponse - 81, // 175: playtesthub.v1.PlaytesthubService.ListSurveyResponses:output_type -> playtesthub.v1.ListSurveyResponsesResponse - 83, // 176: playtesthub.v1.PlaytesthubService.ListAuditLog:output_type -> playtesthub.v1.ListAuditLogResponse - 85, // 177: playtesthub.v1.PlaytesthubService.RetryFailedDms:output_type -> playtesthub.v1.RetryFailedDmsResponse - 88, // 178: playtesthub.v1.PlaytesthubService.GetWorkerHealth:output_type -> playtesthub.v1.GetWorkerHealthResponse - 92, // 179: playtesthub.v1.PlaytesthubService.ListADTLinkages:output_type -> playtesthub.v1.ListADTLinkagesResponse - 94, // 180: playtesthub.v1.PlaytesthubService.StartADTLink:output_type -> playtesthub.v1.StartADTLinkResponse - 96, // 181: playtesthub.v1.PlaytesthubService.CompleteADTLink:output_type -> playtesthub.v1.CompleteADTLinkResponse - 98, // 182: playtesthub.v1.PlaytesthubService.UnlinkADT:output_type -> playtesthub.v1.UnlinkADTResponse - 100, // 183: playtesthub.v1.PlaytesthubService.RecoverADTLinkage:output_type -> playtesthub.v1.RecoverADTLinkageResponse - 102, // 184: playtesthub.v1.PlaytesthubService.ListADTBuilds:output_type -> playtesthub.v1.ListADTBuildsResponse - 105, // 185: playtesthub.v1.PlaytesthubService.ListADTGames:output_type -> playtesthub.v1.ListADTGamesResponse - 109, // 186: playtesthub.v1.PlaytesthubService.GetADTClientDiagnostics:output_type -> playtesthub.v1.GetADTClientDiagnosticsResponse - 107, // 187: playtesthub.v1.PlaytesthubService.ChangeADTBuild:output_type -> playtesthub.v1.ChangeADTBuildResponse - 112, // 188: playtesthub.v1.PlaytesthubService.GetPlaytestParticipants:output_type -> playtesthub.v1.GetPlaytestParticipantsResponse - 115, // 189: playtesthub.v1.PlaytesthubService.CreateAnnouncement:output_type -> playtesthub.v1.CreateAnnouncementResponse - 117, // 190: playtesthub.v1.PlaytesthubService.ListAnnouncements:output_type -> playtesthub.v1.ListAnnouncementsResponse - 147, // [147:191] is the sub-list for method output_type - 103, // [103:147] is the sub-list for method input_type - 103, // [103:103] is the sub-list for extension type_name - 103, // [103:103] is the sub-list for extension extendee - 0, // [0:103] is the sub-list for field type_name + 120, // 5: playtesthub.v1.Playtest.created_at:type_name -> google.protobuf.Timestamp + 120, // 6: playtesthub.v1.Playtest.updated_at:type_name -> google.protobuf.Timestamp + 120, // 7: playtesthub.v1.Playtest.deleted_at:type_name -> google.protobuf.Timestamp + 120, // 8: playtesthub.v1.Playtest.adt_build_checked_at:type_name -> google.protobuf.Timestamp + 0, // 9: playtesthub.v1.PublicPlaytest.platforms:type_name -> playtesthub.v1.Platform + 120, // 10: playtesthub.v1.PublicPlaytest.starts_at:type_name -> google.protobuf.Timestamp + 120, // 11: playtesthub.v1.PublicPlaytest.ends_at:type_name -> google.protobuf.Timestamp + 0, // 12: playtesthub.v1.PlayerPlaytest.platforms:type_name -> playtesthub.v1.Platform + 120, // 13: playtesthub.v1.PlayerPlaytest.starts_at:type_name -> google.protobuf.Timestamp + 120, // 14: playtesthub.v1.PlayerPlaytest.ends_at:type_name -> google.protobuf.Timestamp + 1, // 15: playtesthub.v1.PlayerPlaytest.status:type_name -> playtesthub.v1.PlaytestStatus + 2, // 16: playtesthub.v1.PlayerPlaytest.distribution_model:type_name -> playtesthub.v1.DistributionModel + 0, // 17: playtesthub.v1.Applicant.platforms:type_name -> playtesthub.v1.Platform + 3, // 18: playtesthub.v1.Applicant.status:type_name -> playtesthub.v1.ApplicantStatus + 120, // 19: playtesthub.v1.Applicant.approved_at:type_name -> google.protobuf.Timestamp + 4, // 20: playtesthub.v1.Applicant.last_dm_status:type_name -> playtesthub.v1.DmStatus + 120, // 21: playtesthub.v1.Applicant.last_dm_attempt_at:type_name -> google.protobuf.Timestamp + 120, // 22: playtesthub.v1.Applicant.created_at:type_name -> google.protobuf.Timestamp + 120, // 23: playtesthub.v1.Applicant.survey_response_submitted_at:type_name -> google.protobuf.Timestamp + 120, // 24: playtesthub.v1.NDAAcceptance.accepted_at:type_name -> google.protobuf.Timestamp + 5, // 25: playtesthub.v1.Code.state:type_name -> playtesthub.v1.CodeState + 120, // 26: playtesthub.v1.Code.reserved_at:type_name -> google.protobuf.Timestamp + 120, // 27: playtesthub.v1.Code.granted_at:type_name -> google.protobuf.Timestamp + 120, // 28: playtesthub.v1.Code.created_at:type_name -> google.protobuf.Timestamp + 8, // 29: playtesthub.v1.SurveyQuestion.type:type_name -> playtesthub.v1.SurveyQuestionType + 16, // 30: playtesthub.v1.SurveyQuestion.options:type_name -> playtesthub.v1.MultiChoiceOption + 17, // 31: playtesthub.v1.Survey.questions:type_name -> playtesthub.v1.SurveyQuestion + 120, // 32: playtesthub.v1.Survey.created_at:type_name -> google.protobuf.Timestamp + 19, // 33: playtesthub.v1.SurveyAnswer.multi_choice:type_name -> playtesthub.v1.SurveyMultiChoiceAnswer + 20, // 34: playtesthub.v1.SurveyResponse.answers:type_name -> playtesthub.v1.SurveyAnswer + 120, // 35: playtesthub.v1.SurveyResponse.submitted_at:type_name -> google.protobuf.Timestamp + 120, // 36: playtesthub.v1.AuditLogEntry.created_at:type_name -> google.protobuf.Timestamp + 10, // 37: playtesthub.v1.GetPublicPlaytestResponse.playtest:type_name -> playtesthub.v1.PublicPlaytest + 11, // 38: playtesthub.v1.GetPlaytestForPlayerResponse.playtest:type_name -> playtesthub.v1.PlayerPlaytest + 9, // 39: playtesthub.v1.AdminGetPlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest + 9, // 40: playtesthub.v1.ListPlaytestsResponse.playtests:type_name -> playtesthub.v1.Playtest + 0, // 41: playtesthub.v1.CreatePlaytestRequest.platforms:type_name -> playtesthub.v1.Platform + 120, // 42: playtesthub.v1.CreatePlaytestRequest.starts_at:type_name -> google.protobuf.Timestamp + 120, // 43: playtesthub.v1.CreatePlaytestRequest.ends_at:type_name -> google.protobuf.Timestamp + 2, // 44: playtesthub.v1.CreatePlaytestRequest.distribution_model:type_name -> playtesthub.v1.DistributionModel + 9, // 45: playtesthub.v1.CreatePlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest + 0, // 46: playtesthub.v1.EditPlaytestRequest.platforms:type_name -> playtesthub.v1.Platform + 120, // 47: playtesthub.v1.EditPlaytestRequest.starts_at:type_name -> google.protobuf.Timestamp + 120, // 48: playtesthub.v1.EditPlaytestRequest.ends_at:type_name -> google.protobuf.Timestamp + 9, // 49: playtesthub.v1.EditPlaytestResponse.playtest:type_name -> playtesthub.v1.Playtest + 1, // 50: playtesthub.v1.TransitionPlaytestStatusRequest.target_status:type_name -> playtesthub.v1.PlaytestStatus + 9, // 51: playtesthub.v1.TransitionPlaytestStatusResponse.playtest:type_name -> playtesthub.v1.Playtest + 0, // 52: playtesthub.v1.SignupRequest.platforms:type_name -> playtesthub.v1.Platform + 12, // 53: playtesthub.v1.SignupResponse.applicant:type_name -> playtesthub.v1.Applicant + 12, // 54: playtesthub.v1.GetApplicantStatusResponse.applicant:type_name -> playtesthub.v1.Applicant + 13, // 55: playtesthub.v1.AcceptNDAResponse.acceptance:type_name -> playtesthub.v1.NDAAcceptance + 2, // 56: playtesthub.v1.GetGrantedCodeResponse.distribution_model:type_name -> playtesthub.v1.DistributionModel + 120, // 57: playtesthub.v1.GetADTDownloadInfoResponse.expires_at:type_name -> google.protobuf.Timestamp + 3, // 58: playtesthub.v1.ListApplicantsRequest.status_filter:type_name -> playtesthub.v1.ApplicantStatus + 12, // 59: playtesthub.v1.ListApplicantsResponse.applicants:type_name -> playtesthub.v1.Applicant + 12, // 60: playtesthub.v1.ApproveApplicantResponse.applicant:type_name -> playtesthub.v1.Applicant + 12, // 61: playtesthub.v1.RejectApplicantResponse.applicant:type_name -> playtesthub.v1.Applicant + 12, // 62: playtesthub.v1.RetryDMResponse.applicant:type_name -> playtesthub.v1.Applicant + 64, // 63: playtesthub.v1.UploadCodesResponse.rejections:type_name -> playtesthub.v1.UploadCodesRejection + 15, // 64: playtesthub.v1.TopUpCodesResponse.pool:type_name -> playtesthub.v1.CodePoolStats + 15, // 65: playtesthub.v1.SyncFromAGSResponse.pool:type_name -> playtesthub.v1.CodePoolStats + 15, // 66: playtesthub.v1.GetCodePoolResponse.stats:type_name -> playtesthub.v1.CodePoolStats + 14, // 67: playtesthub.v1.GetCodePoolResponse.codes:type_name -> playtesthub.v1.Code + 17, // 68: playtesthub.v1.CreateSurveyRequest.questions:type_name -> playtesthub.v1.SurveyQuestion + 18, // 69: playtesthub.v1.CreateSurveyResponse.survey:type_name -> playtesthub.v1.Survey + 17, // 70: playtesthub.v1.EditSurveyRequest.questions:type_name -> playtesthub.v1.SurveyQuestion + 18, // 71: playtesthub.v1.EditSurveyResponse.survey:type_name -> playtesthub.v1.Survey + 18, // 72: playtesthub.v1.GetSurveyResponse.survey:type_name -> playtesthub.v1.Survey + 20, // 73: playtesthub.v1.SubmitSurveyResponseRequest.answers:type_name -> playtesthub.v1.SurveyAnswer + 21, // 74: playtesthub.v1.SubmitSurveyResponseResponse.response:type_name -> playtesthub.v1.SurveyResponse + 21, // 75: playtesthub.v1.ListSurveyResponsesResponse.responses:type_name -> playtesthub.v1.SurveyResponse + 22, // 76: playtesthub.v1.ListAuditLogResponse.entries:type_name -> playtesthub.v1.AuditLogEntry + 120, // 77: playtesthub.v1.WorkerHealthEntry.last_tick_at:type_name -> google.protobuf.Timestamp + 120, // 78: playtesthub.v1.WorkerHealthEntry.expires_at:type_name -> google.protobuf.Timestamp + 87, // 79: playtesthub.v1.GetWorkerHealthResponse.workers:type_name -> playtesthub.v1.WorkerHealthEntry + 120, // 80: playtesthub.v1.ADTLinkage.linked_at:type_name -> google.protobuf.Timestamp + 120, // 81: playtesthub.v1.ADTLinkage.deleted_at:type_name -> google.protobuf.Timestamp + 120, // 82: playtesthub.v1.ADTBuild.uploaded_at:type_name -> google.protobuf.Timestamp + 89, // 83: playtesthub.v1.ListADTLinkagesResponse.linkages:type_name -> playtesthub.v1.ADTLinkage + 120, // 84: playtesthub.v1.StartADTLinkResponse.expires_at:type_name -> google.protobuf.Timestamp + 89, // 85: playtesthub.v1.CompleteADTLinkResponse.linkage:type_name -> playtesthub.v1.ADTLinkage + 89, // 86: playtesthub.v1.RecoverADTLinkageResponse.linkage:type_name -> playtesthub.v1.ADTLinkage + 90, // 87: playtesthub.v1.ListADTBuildsResponse.builds:type_name -> playtesthub.v1.ADTBuild + 120, // 88: playtesthub.v1.ADTGame.created_at:type_name -> google.protobuf.Timestamp + 103, // 89: playtesthub.v1.ListADTGamesResponse.games:type_name -> playtesthub.v1.ADTGame + 9, // 90: playtesthub.v1.ChangeADTBuildResponse.playtest:type_name -> playtesthub.v1.Playtest + 9, // 91: playtesthub.v1.CheckADTBuildResponse.playtest:type_name -> playtesthub.v1.Playtest + 120, // 92: playtesthub.v1.ParticipantRow.signup_at:type_name -> google.protobuf.Timestamp + 120, // 93: playtesthub.v1.ParticipantRow.nda_accepted_at:type_name -> google.protobuf.Timestamp + 120, // 94: playtesthub.v1.ParticipantRow.code_sent_at:type_name -> google.protobuf.Timestamp + 3, // 95: playtesthub.v1.ParticipantRow.status:type_name -> playtesthub.v1.ApplicantStatus + 120, // 96: playtesthub.v1.ParticipantRow.adt_download_at:type_name -> google.protobuf.Timestamp + 3, // 97: playtesthub.v1.GetPlaytestParticipantsRequest.status_filter:type_name -> playtesthub.v1.ApplicantStatus + 112, // 98: playtesthub.v1.GetPlaytestParticipantsResponse.participants:type_name -> playtesthub.v1.ParticipantRow + 6, // 99: playtesthub.v1.Announcement.send_to_filter:type_name -> playtesthub.v1.AnnouncementSendToFilter + 7, // 100: playtesthub.v1.Announcement.status:type_name -> playtesthub.v1.AnnouncementStatus + 120, // 101: playtesthub.v1.Announcement.created_at:type_name -> google.protobuf.Timestamp + 6, // 102: playtesthub.v1.CreateAnnouncementRequest.send_to_filter:type_name -> playtesthub.v1.AnnouncementSendToFilter + 115, // 103: playtesthub.v1.CreateAnnouncementResponse.announcement:type_name -> playtesthub.v1.Announcement + 115, // 104: playtesthub.v1.ListAnnouncementsResponse.announcements:type_name -> playtesthub.v1.Announcement + 23, // 105: playtesthub.v1.PlaytesthubService.GetPublicPlaytest:input_type -> playtesthub.v1.GetPublicPlaytestRequest + 25, // 106: playtesthub.v1.PlaytesthubService.GetPublicConfig:input_type -> playtesthub.v1.GetPublicConfigRequest + 27, // 107: playtesthub.v1.PlaytesthubService.GetPlaytestForPlayer:input_type -> playtesthub.v1.GetPlaytestForPlayerRequest + 29, // 108: playtesthub.v1.PlaytesthubService.WhoAmI:input_type -> playtesthub.v1.WhoAmIRequest + 31, // 109: playtesthub.v1.PlaytesthubService.AdminGetPlaytest:input_type -> playtesthub.v1.AdminGetPlaytestRequest + 33, // 110: playtesthub.v1.PlaytesthubService.ListPlaytests:input_type -> playtesthub.v1.ListPlaytestsRequest + 35, // 111: playtesthub.v1.PlaytesthubService.CreatePlaytest:input_type -> playtesthub.v1.CreatePlaytestRequest + 37, // 112: playtesthub.v1.PlaytesthubService.EditPlaytest:input_type -> playtesthub.v1.EditPlaytestRequest + 39, // 113: playtesthub.v1.PlaytesthubService.SoftDeletePlaytest:input_type -> playtesthub.v1.SoftDeletePlaytestRequest + 41, // 114: playtesthub.v1.PlaytesthubService.TransitionPlaytestStatus:input_type -> playtesthub.v1.TransitionPlaytestStatusRequest + 47, // 115: playtesthub.v1.PlaytesthubService.ExchangeDiscordCode:input_type -> playtesthub.v1.ExchangeDiscordCodeRequest + 43, // 116: playtesthub.v1.PlaytesthubService.Signup:input_type -> playtesthub.v1.SignupRequest + 45, // 117: playtesthub.v1.PlaytesthubService.GetApplicantStatus:input_type -> playtesthub.v1.GetApplicantStatusRequest + 49, // 118: playtesthub.v1.PlaytesthubService.AcceptNDA:input_type -> playtesthub.v1.AcceptNDARequest + 51, // 119: playtesthub.v1.PlaytesthubService.GetGrantedCode:input_type -> playtesthub.v1.GetGrantedCodeRequest + 53, // 120: playtesthub.v1.PlaytesthubService.GetADTDownloadInfo:input_type -> playtesthub.v1.GetADTDownloadInfoRequest + 55, // 121: playtesthub.v1.PlaytesthubService.ListApplicants:input_type -> playtesthub.v1.ListApplicantsRequest + 57, // 122: playtesthub.v1.PlaytesthubService.ApproveApplicant:input_type -> playtesthub.v1.ApproveApplicantRequest + 59, // 123: playtesthub.v1.PlaytesthubService.RejectApplicant:input_type -> playtesthub.v1.RejectApplicantRequest + 61, // 124: playtesthub.v1.PlaytesthubService.RetryDM:input_type -> playtesthub.v1.RetryDMRequest + 63, // 125: playtesthub.v1.PlaytesthubService.UploadCodes:input_type -> playtesthub.v1.UploadCodesRequest + 66, // 126: playtesthub.v1.PlaytesthubService.TopUpCodes:input_type -> playtesthub.v1.TopUpCodesRequest + 68, // 127: playtesthub.v1.PlaytesthubService.SyncFromAGS:input_type -> playtesthub.v1.SyncFromAGSRequest + 70, // 128: playtesthub.v1.PlaytesthubService.GetCodePool:input_type -> playtesthub.v1.GetCodePoolRequest + 72, // 129: playtesthub.v1.PlaytesthubService.CreateSurvey:input_type -> playtesthub.v1.CreateSurveyRequest + 74, // 130: playtesthub.v1.PlaytesthubService.EditSurvey:input_type -> playtesthub.v1.EditSurveyRequest + 76, // 131: playtesthub.v1.PlaytesthubService.GetSurvey:input_type -> playtesthub.v1.GetSurveyRequest + 78, // 132: playtesthub.v1.PlaytesthubService.SubmitSurveyResponse:input_type -> playtesthub.v1.SubmitSurveyResponseRequest + 80, // 133: playtesthub.v1.PlaytesthubService.ListSurveyResponses:input_type -> playtesthub.v1.ListSurveyResponsesRequest + 82, // 134: playtesthub.v1.PlaytesthubService.ListAuditLog:input_type -> playtesthub.v1.ListAuditLogRequest + 84, // 135: playtesthub.v1.PlaytesthubService.RetryFailedDms:input_type -> playtesthub.v1.RetryFailedDmsRequest + 86, // 136: playtesthub.v1.PlaytesthubService.GetWorkerHealth:input_type -> playtesthub.v1.GetWorkerHealthRequest + 91, // 137: playtesthub.v1.PlaytesthubService.ListADTLinkages:input_type -> playtesthub.v1.ListADTLinkagesRequest + 93, // 138: playtesthub.v1.PlaytesthubService.StartADTLink:input_type -> playtesthub.v1.StartADTLinkRequest + 95, // 139: playtesthub.v1.PlaytesthubService.CompleteADTLink:input_type -> playtesthub.v1.CompleteADTLinkRequest + 97, // 140: playtesthub.v1.PlaytesthubService.UnlinkADT:input_type -> playtesthub.v1.UnlinkADTRequest + 99, // 141: playtesthub.v1.PlaytesthubService.RecoverADTLinkage:input_type -> playtesthub.v1.RecoverADTLinkageRequest + 101, // 142: playtesthub.v1.PlaytesthubService.ListADTBuilds:input_type -> playtesthub.v1.ListADTBuildsRequest + 104, // 143: playtesthub.v1.PlaytesthubService.ListADTGames:input_type -> playtesthub.v1.ListADTGamesRequest + 110, // 144: playtesthub.v1.PlaytesthubService.GetADTClientDiagnostics:input_type -> playtesthub.v1.GetADTClientDiagnosticsRequest + 106, // 145: playtesthub.v1.PlaytesthubService.ChangeADTBuild:input_type -> playtesthub.v1.ChangeADTBuildRequest + 108, // 146: playtesthub.v1.PlaytesthubService.CheckADTBuild:input_type -> playtesthub.v1.CheckADTBuildRequest + 113, // 147: playtesthub.v1.PlaytesthubService.GetPlaytestParticipants:input_type -> playtesthub.v1.GetPlaytestParticipantsRequest + 116, // 148: playtesthub.v1.PlaytesthubService.CreateAnnouncement:input_type -> playtesthub.v1.CreateAnnouncementRequest + 118, // 149: playtesthub.v1.PlaytesthubService.ListAnnouncements:input_type -> playtesthub.v1.ListAnnouncementsRequest + 24, // 150: playtesthub.v1.PlaytesthubService.GetPublicPlaytest:output_type -> playtesthub.v1.GetPublicPlaytestResponse + 26, // 151: playtesthub.v1.PlaytesthubService.GetPublicConfig:output_type -> playtesthub.v1.GetPublicConfigResponse + 28, // 152: playtesthub.v1.PlaytesthubService.GetPlaytestForPlayer:output_type -> playtesthub.v1.GetPlaytestForPlayerResponse + 30, // 153: playtesthub.v1.PlaytesthubService.WhoAmI:output_type -> playtesthub.v1.WhoAmIResponse + 32, // 154: playtesthub.v1.PlaytesthubService.AdminGetPlaytest:output_type -> playtesthub.v1.AdminGetPlaytestResponse + 34, // 155: playtesthub.v1.PlaytesthubService.ListPlaytests:output_type -> playtesthub.v1.ListPlaytestsResponse + 36, // 156: playtesthub.v1.PlaytesthubService.CreatePlaytest:output_type -> playtesthub.v1.CreatePlaytestResponse + 38, // 157: playtesthub.v1.PlaytesthubService.EditPlaytest:output_type -> playtesthub.v1.EditPlaytestResponse + 40, // 158: playtesthub.v1.PlaytesthubService.SoftDeletePlaytest:output_type -> playtesthub.v1.SoftDeletePlaytestResponse + 42, // 159: playtesthub.v1.PlaytesthubService.TransitionPlaytestStatus:output_type -> playtesthub.v1.TransitionPlaytestStatusResponse + 48, // 160: playtesthub.v1.PlaytesthubService.ExchangeDiscordCode:output_type -> playtesthub.v1.ExchangeDiscordCodeResponse + 44, // 161: playtesthub.v1.PlaytesthubService.Signup:output_type -> playtesthub.v1.SignupResponse + 46, // 162: playtesthub.v1.PlaytesthubService.GetApplicantStatus:output_type -> playtesthub.v1.GetApplicantStatusResponse + 50, // 163: playtesthub.v1.PlaytesthubService.AcceptNDA:output_type -> playtesthub.v1.AcceptNDAResponse + 52, // 164: playtesthub.v1.PlaytesthubService.GetGrantedCode:output_type -> playtesthub.v1.GetGrantedCodeResponse + 54, // 165: playtesthub.v1.PlaytesthubService.GetADTDownloadInfo:output_type -> playtesthub.v1.GetADTDownloadInfoResponse + 56, // 166: playtesthub.v1.PlaytesthubService.ListApplicants:output_type -> playtesthub.v1.ListApplicantsResponse + 58, // 167: playtesthub.v1.PlaytesthubService.ApproveApplicant:output_type -> playtesthub.v1.ApproveApplicantResponse + 60, // 168: playtesthub.v1.PlaytesthubService.RejectApplicant:output_type -> playtesthub.v1.RejectApplicantResponse + 62, // 169: playtesthub.v1.PlaytesthubService.RetryDM:output_type -> playtesthub.v1.RetryDMResponse + 65, // 170: playtesthub.v1.PlaytesthubService.UploadCodes:output_type -> playtesthub.v1.UploadCodesResponse + 67, // 171: playtesthub.v1.PlaytesthubService.TopUpCodes:output_type -> playtesthub.v1.TopUpCodesResponse + 69, // 172: playtesthub.v1.PlaytesthubService.SyncFromAGS:output_type -> playtesthub.v1.SyncFromAGSResponse + 71, // 173: playtesthub.v1.PlaytesthubService.GetCodePool:output_type -> playtesthub.v1.GetCodePoolResponse + 73, // 174: playtesthub.v1.PlaytesthubService.CreateSurvey:output_type -> playtesthub.v1.CreateSurveyResponse + 75, // 175: playtesthub.v1.PlaytesthubService.EditSurvey:output_type -> playtesthub.v1.EditSurveyResponse + 77, // 176: playtesthub.v1.PlaytesthubService.GetSurvey:output_type -> playtesthub.v1.GetSurveyResponse + 79, // 177: playtesthub.v1.PlaytesthubService.SubmitSurveyResponse:output_type -> playtesthub.v1.SubmitSurveyResponseResponse + 81, // 178: playtesthub.v1.PlaytesthubService.ListSurveyResponses:output_type -> playtesthub.v1.ListSurveyResponsesResponse + 83, // 179: playtesthub.v1.PlaytesthubService.ListAuditLog:output_type -> playtesthub.v1.ListAuditLogResponse + 85, // 180: playtesthub.v1.PlaytesthubService.RetryFailedDms:output_type -> playtesthub.v1.RetryFailedDmsResponse + 88, // 181: playtesthub.v1.PlaytesthubService.GetWorkerHealth:output_type -> playtesthub.v1.GetWorkerHealthResponse + 92, // 182: playtesthub.v1.PlaytesthubService.ListADTLinkages:output_type -> playtesthub.v1.ListADTLinkagesResponse + 94, // 183: playtesthub.v1.PlaytesthubService.StartADTLink:output_type -> playtesthub.v1.StartADTLinkResponse + 96, // 184: playtesthub.v1.PlaytesthubService.CompleteADTLink:output_type -> playtesthub.v1.CompleteADTLinkResponse + 98, // 185: playtesthub.v1.PlaytesthubService.UnlinkADT:output_type -> playtesthub.v1.UnlinkADTResponse + 100, // 186: playtesthub.v1.PlaytesthubService.RecoverADTLinkage:output_type -> playtesthub.v1.RecoverADTLinkageResponse + 102, // 187: playtesthub.v1.PlaytesthubService.ListADTBuilds:output_type -> playtesthub.v1.ListADTBuildsResponse + 105, // 188: playtesthub.v1.PlaytesthubService.ListADTGames:output_type -> playtesthub.v1.ListADTGamesResponse + 111, // 189: playtesthub.v1.PlaytesthubService.GetADTClientDiagnostics:output_type -> playtesthub.v1.GetADTClientDiagnosticsResponse + 107, // 190: playtesthub.v1.PlaytesthubService.ChangeADTBuild:output_type -> playtesthub.v1.ChangeADTBuildResponse + 109, // 191: playtesthub.v1.PlaytesthubService.CheckADTBuild:output_type -> playtesthub.v1.CheckADTBuildResponse + 114, // 192: playtesthub.v1.PlaytesthubService.GetPlaytestParticipants:output_type -> playtesthub.v1.GetPlaytestParticipantsResponse + 117, // 193: playtesthub.v1.PlaytesthubService.CreateAnnouncement:output_type -> playtesthub.v1.CreateAnnouncementResponse + 119, // 194: playtesthub.v1.PlaytesthubService.ListAnnouncements:output_type -> playtesthub.v1.ListAnnouncementsResponse + 150, // [150:195] is the sub-list for method output_type + 105, // [105:150] is the sub-list for method input_type + 105, // [105:105] is the sub-list for extension type_name + 105, // [105:105] is the sub-list for extension extendee + 0, // [0:105] is the sub-list for field type_name } func init() { file_playtesthub_v1_playtesthub_proto_init() } @@ -8621,14 +8772,14 @@ func file_playtesthub_v1_playtesthub_proto_init() { file_playtesthub_v1_playtesthub_proto_msgTypes[26].OneofWrappers = []any{} file_playtesthub_v1_playtesthub_proto_msgTypes[28].OneofWrappers = []any{} file_playtesthub_v1_playtesthub_proto_msgTypes[50].OneofWrappers = []any{} - file_playtesthub_v1_playtesthub_proto_msgTypes[101].OneofWrappers = []any{} + file_playtesthub_v1_playtesthub_proto_msgTypes[103].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_playtesthub_v1_playtesthub_proto_rawDesc), len(file_playtesthub_v1_playtesthub_proto_rawDesc)), NumEnums: 9, - NumMessages: 109, + NumMessages: 111, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pb/playtesthub/v1/playtesthub.pb.gw.go b/pkg/pb/playtesthub/v1/playtesthub.pb.gw.go index 8cc319c..5343757 100644 --- a/pkg/pb/playtesthub/v1/playtesthub.pb.gw.go +++ b/pkg/pb/playtesthub/v1/playtesthub.pb.gw.go @@ -2092,6 +2092,67 @@ func local_request_PlaytesthubService_ChangeADTBuild_0(ctx context.Context, mars return msg, metadata, err } +func request_PlaytesthubService_CheckADTBuild_0(ctx context.Context, marshaler runtime.Marshaler, client PlaytesthubServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CheckADTBuildRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["namespace"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace") + } + protoReq.Namespace, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err) + } + val, ok = pathParams["playtest_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playtest_id") + } + protoReq.PlaytestId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playtest_id", err) + } + msg, err := client.CheckADTBuild(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_PlaytesthubService_CheckADTBuild_0(ctx context.Context, marshaler runtime.Marshaler, server PlaytesthubServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CheckADTBuildRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["namespace"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace") + } + protoReq.Namespace, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err) + } + val, ok = pathParams["playtest_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playtest_id") + } + protoReq.PlaytestId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playtest_id", err) + } + msg, err := server.CheckADTBuild(ctx, &protoReq) + return msg, metadata, err +} + var filter_PlaytesthubService_GetPlaytestParticipants_0 = &utilities.DoubleArray{Encoding: map[string]int{"namespace": 0, "playtest_id": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} func request_PlaytesthubService_GetPlaytestParticipants_0(ctx context.Context, marshaler runtime.Marshaler, client PlaytesthubServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { @@ -3103,6 +3164,26 @@ func RegisterPlaytesthubServiceHandlerServer(ctx context.Context, mux *runtime.S } forward_PlaytesthubService_ChangeADTBuild_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_PlaytesthubService_CheckADTBuild_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/playtesthub.v1.PlaytesthubService/CheckADTBuild", runtime.WithHTTPPathPattern("/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:check")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_PlaytesthubService_CheckADTBuild_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlaytesthubService_CheckADTBuild_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_PlaytesthubService_GetPlaytestParticipants_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3900,6 +3981,23 @@ func RegisterPlaytesthubServiceHandlerClient(ctx context.Context, mux *runtime.S } forward_PlaytesthubService_ChangeADTBuild_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_PlaytesthubService_CheckADTBuild_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/playtesthub.v1.PlaytesthubService/CheckADTBuild", runtime.WithHTTPPathPattern("/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:check")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_PlaytesthubService_CheckADTBuild_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlaytesthubService_CheckADTBuild_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_PlaytesthubService_GetPlaytestParticipants_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3996,6 +4094,7 @@ var ( pattern_PlaytesthubService_ListADTGames_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 2, 5, 1, 0, 4, 1, 5, 6, 2, 7}, []string{"v1", "admin", "namespaces", "namespace", "adt", "linkages", "adt_linkage_id", "games"}, "")) pattern_PlaytesthubService_GetADTClientDiagnostics_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 2, 5}, []string{"v1", "admin", "namespaces", "namespace", "diagnostics", "adt-client-kind"}, "")) pattern_PlaytesthubService_ChangeADTBuild_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 2, 7}, []string{"v1", "admin", "namespaces", "namespace", "playtests", "playtest_id", "adt", "build"}, "change")) + pattern_PlaytesthubService_CheckADTBuild_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 2, 7}, []string{"v1", "admin", "namespaces", "namespace", "playtests", "playtest_id", "adt", "build"}, "check")) pattern_PlaytesthubService_GetPlaytestParticipants_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"v1", "admin", "namespaces", "namespace", "playtests", "playtest_id", "participants"}, "")) pattern_PlaytesthubService_CreateAnnouncement_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"v1", "admin", "namespaces", "namespace", "playtests", "playtest_id", "announcements"}, "")) pattern_PlaytesthubService_ListAnnouncements_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"v1", "admin", "namespaces", "namespace", "playtests", "playtest_id", "announcements"}, "")) @@ -4043,6 +4142,7 @@ var ( forward_PlaytesthubService_ListADTGames_0 = runtime.ForwardResponseMessage forward_PlaytesthubService_GetADTClientDiagnostics_0 = runtime.ForwardResponseMessage forward_PlaytesthubService_ChangeADTBuild_0 = runtime.ForwardResponseMessage + forward_PlaytesthubService_CheckADTBuild_0 = runtime.ForwardResponseMessage forward_PlaytesthubService_GetPlaytestParticipants_0 = runtime.ForwardResponseMessage forward_PlaytesthubService_CreateAnnouncement_0 = runtime.ForwardResponseMessage forward_PlaytesthubService_ListAnnouncements_0 = runtime.ForwardResponseMessage diff --git a/pkg/pb/playtesthub/v1/playtesthub_grpc.pb.go b/pkg/pb/playtesthub/v1/playtesthub_grpc.pb.go index ac18821..3ab9cd9 100644 --- a/pkg/pb/playtesthub/v1/playtesthub_grpc.pb.go +++ b/pkg/pb/playtesthub/v1/playtesthub_grpc.pb.go @@ -60,6 +60,7 @@ const ( PlaytesthubService_ListADTGames_FullMethodName = "/playtesthub.v1.PlaytesthubService/ListADTGames" PlaytesthubService_GetADTClientDiagnostics_FullMethodName = "/playtesthub.v1.PlaytesthubService/GetADTClientDiagnostics" PlaytesthubService_ChangeADTBuild_FullMethodName = "/playtesthub.v1.PlaytesthubService/ChangeADTBuild" + PlaytesthubService_CheckADTBuild_FullMethodName = "/playtesthub.v1.PlaytesthubService/CheckADTBuild" PlaytesthubService_GetPlaytestParticipants_FullMethodName = "/playtesthub.v1.PlaytesthubService/GetPlaytestParticipants" PlaytesthubService_CreateAnnouncement_FullMethodName = "/playtesthub.v1.PlaytesthubService/CreateAnnouncement" PlaytesthubService_ListAnnouncements_FullMethodName = "/playtesthub.v1.PlaytesthubService/ListAnnouncements" @@ -130,6 +131,14 @@ type PlaytesthubServiceClient interface { // (adt_game_id, adt_build_id) pair against the linkage via ListBuilds // before persisting. ChangeADTBuild(ctx context.Context, in *ChangeADTBuildRequest, opts ...grpc.CallOption) (*ChangeADTBuildResponse, error) + // CheckADTBuild probes whether the playtest's current ADT build can still + // mint a download URL — the same adt.Client.IssueDownloadURL call + // ApproveApplicant makes — and persists the result to adt_build_status + + // adt_build_checked_at so the detail page surfaces a gone/undownloadable + // build without an approval attempt. Side effect: ADT mints a throwaway + // URL on success, so this is an explicit on-demand action, not a passive + // read. + CheckADTBuild(ctx context.Context, in *CheckADTBuildRequest, opts ...grpc.CallOption) (*CheckADTBuildResponse, error) // M5.C — participants surface backing the detail page Participants tab. // Cache-only read; the four ADT telemetry cache columns ship dormant in // M5.C (NULL/zero across the milestone — M6 lights them up). @@ -559,6 +568,16 @@ func (c *playtesthubServiceClient) ChangeADTBuild(ctx context.Context, in *Chang return out, nil } +func (c *playtesthubServiceClient) CheckADTBuild(ctx context.Context, in *CheckADTBuildRequest, opts ...grpc.CallOption) (*CheckADTBuildResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckADTBuildResponse) + err := c.cc.Invoke(ctx, PlaytesthubService_CheckADTBuild_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *playtesthubServiceClient) GetPlaytestParticipants(ctx context.Context, in *GetPlaytestParticipantsRequest, opts ...grpc.CallOption) (*GetPlaytestParticipantsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetPlaytestParticipantsResponse) @@ -654,6 +673,14 @@ type PlaytesthubServiceServer interface { // (adt_game_id, adt_build_id) pair against the linkage via ListBuilds // before persisting. ChangeADTBuild(context.Context, *ChangeADTBuildRequest) (*ChangeADTBuildResponse, error) + // CheckADTBuild probes whether the playtest's current ADT build can still + // mint a download URL — the same adt.Client.IssueDownloadURL call + // ApproveApplicant makes — and persists the result to adt_build_status + + // adt_build_checked_at so the detail page surfaces a gone/undownloadable + // build without an approval attempt. Side effect: ADT mints a throwaway + // URL on success, so this is an explicit on-demand action, not a passive + // read. + CheckADTBuild(context.Context, *CheckADTBuildRequest) (*CheckADTBuildResponse, error) // M5.C — participants surface backing the detail page Participants tab. // Cache-only read; the four ADT telemetry cache columns ship dormant in // M5.C (NULL/zero across the milestone — M6 lights them up). @@ -795,6 +822,9 @@ func (UnimplementedPlaytesthubServiceServer) GetADTClientDiagnostics(context.Con func (UnimplementedPlaytesthubServiceServer) ChangeADTBuild(context.Context, *ChangeADTBuildRequest) (*ChangeADTBuildResponse, error) { return nil, status.Error(codes.Unimplemented, "method ChangeADTBuild not implemented") } +func (UnimplementedPlaytesthubServiceServer) CheckADTBuild(context.Context, *CheckADTBuildRequest) (*CheckADTBuildResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CheckADTBuild not implemented") +} func (UnimplementedPlaytesthubServiceServer) GetPlaytestParticipants(context.Context, *GetPlaytestParticipantsRequest) (*GetPlaytestParticipantsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetPlaytestParticipants not implemented") } @@ -1562,6 +1592,24 @@ func _PlaytesthubService_ChangeADTBuild_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _PlaytesthubService_CheckADTBuild_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckADTBuildRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaytesthubServiceServer).CheckADTBuild(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PlaytesthubService_CheckADTBuild_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaytesthubServiceServer).CheckADTBuild(ctx, req.(*CheckADTBuildRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _PlaytesthubService_GetPlaytestParticipants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetPlaytestParticipantsRequest) if err := dec(in); err != nil { @@ -1787,6 +1835,10 @@ var PlaytesthubService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ChangeADTBuild", Handler: _PlaytesthubService_ChangeADTBuild_Handler, }, + { + MethodName: "CheckADTBuild", + Handler: _PlaytesthubService_CheckADTBuild_Handler, + }, { MethodName: "GetPlaytestParticipants", Handler: _PlaytesthubService_GetPlaytestParticipants_Handler, diff --git a/pkg/repo/adt_build_health_test.go b/pkg/repo/adt_build_health_test.go new file mode 100644 index 0000000..a7e989c --- /dev/null +++ b/pkg/repo/adt_build_health_test.go @@ -0,0 +1,187 @@ +package repo_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgconn" + + "github.com/anggorodewanto/playtesthub/pkg/repo" +) + +func newADTPlaytest(slug string) *repo.Playtest { + ns, game, build := "adt-ns", "game-1", "build-1" + return &repo.Playtest{ + Namespace: testNamespace, + Slug: slug, + Title: "ADT playtest", + Platforms: []string{"PC"}, + Status: "DRAFT", + DistributionModel: "ADT", + ADTNamespace: &ns, + ADTGameID: &game, + ADTBuildID: &build, + } +} + +// TestMigration0009_PlaytestADTBuildHealthColumns pins the two columns +// added to playtest by migration 0009. +func TestMigration0009_PlaytestADTBuildHealthColumns(t *testing.T) { + truncateAll(t) + ctx := context.Background() + + const sql = ` + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'playtest' + AND column_name IN ('adt_build_status', 'adt_build_checked_at') + ORDER BY column_name` + + rows, err := testPool.Query(ctx, sql) + if err != nil { + t.Fatalf("query columns: %v", err) + } + defer rows.Close() + + type col struct{ name, dataType, isNullable string } + var got []col + for rows.Next() { + var c col + if scanErr := rows.Scan(&c.name, &c.dataType, &c.isNullable); scanErr != nil { + t.Fatalf("scan: %v", scanErr) + } + got = append(got, c) + } + if rows.Err() != nil { + t.Fatalf("rows.Err: %v", rows.Err()) + } + + want := []col{ + {"adt_build_checked_at", "timestamp with time zone", "YES"}, + {"adt_build_status", "text", "YES"}, + } + if len(got) != len(want) { + t.Fatalf("got %d rows, want %d: %+v", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("row[%d] = %+v, want %+v", i, got[i], want[i]) + } + } +} + +// TestMigration0009_BuildStatusEnumCheck pins the CHECK constraint: +// adt_build_status must be NULL, 'OK', or 'UNAVAILABLE'. +func TestMigration0009_BuildStatusEnumCheck(t *testing.T) { + truncateAll(t) + ctx := context.Background() + + _, err := testPool.Exec(ctx, + `INSERT INTO playtest (namespace, slug, title, distribution_model, adt_namespace, adt_game_id, adt_build_id, adt_build_status) + VALUES ($1, $2, $3, 'ADT', $4, $5, $6, 'BOGUS')`, + "ns", "adt-bad-status", "title", "adt-ns", "g", "b") + if err == nil { + t.Fatalf("BOGUS adt_build_status accepted; want CHECK rejection") + } + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) || pgErr.ConstraintName != "playtest_adt_build_status_enum" { + t.Errorf("err = %v; want playtest_adt_build_status_enum violation", err) + } +} + +// TestPlaytestUpdateADTBuild_Persists is the regression guard for the +// ChangeADTBuild persistence bug: the generic Update omits adt_game_id / +// adt_build_id, so a dedicated writer must round-trip them through real +// Postgres (the service-layer fake masked this). +func TestPlaytestUpdateADTBuild_Persists(t *testing.T) { + truncateAll(t) + store := repo.NewPgPlaytestStore(testPool) + ctx := context.Background() + + created, err := store.Create(ctx, newADTPlaytest("adt-update-build")) + if err != nil { + t.Fatalf("Create: %v", err) + } + + updated, err := store.UpdateADTBuild(ctx, testNamespace, created.ID, "game-2", "build-2") + if err != nil { + t.Fatalf("UpdateADTBuild: %v", err) + } + if updated.ADTGameID == nil || *updated.ADTGameID != "game-2" { + t.Errorf("returned adt_game_id = %v, want game-2", updated.ADTGameID) + } + if updated.ADTBuildID == nil || *updated.ADTBuildID != "build-2" { + t.Errorf("returned adt_build_id = %v, want build-2", updated.ADTBuildID) + } + + // Re-read from the DB — the change must have actually persisted. + got, err := store.GetByID(ctx, testNamespace, created.ID) + if err != nil { + t.Fatalf("GetByID: %v", err) + } + if got.ADTGameID == nil || *got.ADTGameID != "game-2" { + t.Errorf("persisted adt_game_id = %v, want game-2", got.ADTGameID) + } + if got.ADTBuildID == nil || *got.ADTBuildID != "build-2" { + t.Errorf("persisted adt_build_id = %v, want build-2", got.ADTBuildID) + } +} + +func TestPlaytestUpdateADTBuild_RefusesSoftDeleted(t *testing.T) { + truncateAll(t) + store := repo.NewPgPlaytestStore(testPool) + ctx := context.Background() + + created, err := store.Create(ctx, newADTPlaytest("adt-update-deleted")) + if err != nil { + t.Fatalf("Create: %v", err) + } + if delErr := store.SoftDelete(ctx, testNamespace, created.ID); delErr != nil { + t.Fatalf("SoftDelete: %v", delErr) + } + if _, err = store.UpdateADTBuild(ctx, testNamespace, created.ID, "g", "b"); !errors.Is(err, repo.ErrNotFound) { + t.Errorf("UpdateADTBuild on soft-deleted: err = %v, want ErrNotFound", err) + } +} + +// TestPlaytestSetADTBuildHealth_Persists round-trips the health columns +// and confirms updated_at is NOT bumped (a health probe is not an edit). +func TestPlaytestSetADTBuildHealth_Persists(t *testing.T) { + truncateAll(t) + store := repo.NewPgPlaytestStore(testPool) + ctx := context.Background() + + created, err := store.Create(ctx, newADTPlaytest("adt-health")) + if err != nil { + t.Fatalf("Create: %v", err) + } + if created.ADTBuildStatus != nil { + t.Errorf("new row adt_build_status = %v, want nil", created.ADTBuildStatus) + } + + checkedAt := time.Now().UTC().Truncate(time.Millisecond) + updated, err := store.SetADTBuildHealth(ctx, testNamespace, created.ID, "UNAVAILABLE", checkedAt) + if err != nil { + t.Fatalf("SetADTBuildHealth: %v", err) + } + if updated.ADTBuildStatus == nil || *updated.ADTBuildStatus != "UNAVAILABLE" { + t.Errorf("adt_build_status = %v, want UNAVAILABLE", updated.ADTBuildStatus) + } + if updated.ADTBuildCheckedAt == nil || !updated.ADTBuildCheckedAt.Equal(checkedAt) { + t.Errorf("adt_build_checked_at = %v, want %v", updated.ADTBuildCheckedAt, checkedAt) + } + if !updated.UpdatedAt.Equal(created.UpdatedAt) { + t.Errorf("updated_at changed: got %v, want unchanged %v (health write must not touch updated_at)", updated.UpdatedAt, created.UpdatedAt) + } + + got, err := store.GetByID(ctx, testNamespace, created.ID) + if err != nil { + t.Fatalf("GetByID: %v", err) + } + if got.ADTBuildStatus == nil || *got.ADTBuildStatus != "UNAVAILABLE" { + t.Errorf("persisted adt_build_status = %v, want UNAVAILABLE", got.ADTBuildStatus) + } +} diff --git a/pkg/repo/playtest.go b/pkg/repo/playtest.go index 793cc50..7f4fe4e 100644 --- a/pkg/repo/playtest.go +++ b/pkg/repo/playtest.go @@ -48,14 +48,22 @@ type Playtest struct { ADTGameID *string ADTBuildID *string ADTFallbackDownloadURL *string - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time + // ADTBuildStatus / ADTBuildCheckedAt — M5.C build-health surfacing. + // ADTBuildStatus is nil (never checked / non-ADT), "OK", or + // "UNAVAILABLE" (ADT build-not-found). Written by SetADTBuildHealth, + // not the generic Update. See migration 0009. + ADTBuildStatus *string + ADTBuildCheckedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time } // PlaytestStore is the data access surface the service layer depends on. // Mocks for unit tests (phase 6) are generated against this interface; // PgPlaytestStore is the only production implementation. +// +//nolint:interfacebloat // single source of truth for playtest persistence; the ADT build mutators are deliberate distinct writers (see UpdateADTBuild / SetADTBuildHealth docs). type PlaytestStore interface { Create(ctx context.Context, p *Playtest) (*Playtest, error) // CreateTx is the tx-bound variant used by the AGS_CAMPAIGN @@ -82,6 +90,19 @@ type PlaytestStore interface { // spec"). Returns ErrNotFound when the playtest is missing or // soft-deleted. SetSurveyID(ctx context.Context, namespace string, playtestID, surveyID uuid.UUID) error + // UpdateADTBuild repoints an ADT playtest at a new (adt_game_id, + // adt_build_id) pair. Distinct from Update, whose SET whitelist + // deliberately omits the build identifiers — ChangeADTBuild needs to + // persist them and must not route through the generic editor (which + // would silently drop them). Returns ErrNotFound when the row is + // missing or soft-deleted. + UpdateADTBuild(ctx context.Context, namespace string, id uuid.UUID, adtGameID, adtBuildID string) (*Playtest, error) + // SetADTBuildHealth records the last observed ADT build download + // health (M5.C "build gone" surfacing). status is "OK" or + // "UNAVAILABLE"; checkedAt is the observation time. Does NOT touch + // updated_at — a health probe is not a content edit. Returns + // ErrNotFound when the row is missing or soft-deleted. + SetADTBuildHealth(ctx context.Context, namespace string, id uuid.UUID, status string, checkedAt time.Time) (*Playtest, error) } // PgPlaytestStore is the Postgres-backed PlaytestStore. @@ -100,6 +121,7 @@ const playtestColumns = ` ags_item_id, ags_campaign_id, initial_code_quantity, auto_approve, auto_approve_limit, adt_namespace, adt_game_id, adt_build_id, adt_fallback_download_url, + adt_build_status, adt_build_checked_at, created_at, updated_at, deleted_at` // Create inserts a new playtest row. The caller supplies business @@ -267,6 +289,56 @@ func (s *PgPlaytestStore) Update(ctx context.Context, p *Playtest) (*Playtest, e return got, nil } +// UpdateADTBuild persists a new (adt_game_id, adt_build_id) pair for an +// ADT playtest. The generic Update omits these columns on purpose, so +// ChangeADTBuild routes here; without it the build change is accepted by +// the service layer but never reaches Postgres. +func (s *PgPlaytestStore) UpdateADTBuild(ctx context.Context, namespace string, id uuid.UUID, adtGameID, adtBuildID string) (*Playtest, error) { + const sql = ` + UPDATE playtest + SET adt_game_id = $3, + adt_build_id = $4, + updated_at = NOW() + WHERE namespace = $1 + AND id = $2 + AND deleted_at IS NULL + RETURNING ` + playtestColumns + + row := s.pool.QueryRow(ctx, sql, namespace, id, adtGameID, adtBuildID) + got, err := scanPlaytest(row) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("updating playtest adt build: %w", classifyPgError(err)) + } + return got, nil +} + +// SetADTBuildHealth writes the two health columns only — never +// updated_at, since an approve-time probe or an on-demand check is not a +// content edit and must not bump the row's edit timestamp. +func (s *PgPlaytestStore) SetADTBuildHealth(ctx context.Context, namespace string, id uuid.UUID, status string, checkedAt time.Time) (*Playtest, error) { + const sql = ` + UPDATE playtest + SET adt_build_status = $3, + adt_build_checked_at = $4 + WHERE namespace = $1 + AND id = $2 + AND deleted_at IS NULL + RETURNING ` + playtestColumns + + row := s.pool.QueryRow(ctx, sql, namespace, id, status, checkedAt) + got, err := scanPlaytest(row) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("setting playtest adt build health: %w", classifyPgError(err)) + } + return got, nil +} + // SoftDelete marks the row deleted. Subsequent Create calls that reuse // the slug fail with ErrUniqueViolation (PRD §5.1 slug uniqueness // spans live and soft-deleted rows). @@ -395,6 +467,8 @@ func scanPlaytest(row pgx.Row) (*Playtest, error) { adtGameID pgtype.Text adtBuildID pgtype.Text adtFallback pgtype.Text + adtBuildStat pgtype.Text + adtBuildChk pgtype.Timestamptz deletedAt pgtype.Timestamptz ) err := row.Scan( @@ -422,6 +496,8 @@ func scanPlaytest(row pgx.Row) (*Playtest, error) { &adtGameID, &adtBuildID, &adtFallback, + &adtBuildStat, + &adtBuildChk, &p.CreatedAt, &p.UpdatedAt, &deletedAt, @@ -440,6 +516,8 @@ func scanPlaytest(row pgx.Row) (*Playtest, error) { p.ADTGameID = stringPtrFromPg(adtGameID) p.ADTBuildID = stringPtrFromPg(adtBuildID) p.ADTFallbackDownloadURL = stringPtrFromPg(adtFallback) + p.ADTBuildStatus = stringPtrFromPg(adtBuildStat) + p.ADTBuildCheckedAt = timePtrFromPg(adtBuildChk) p.DeletedAt = timePtrFromPg(deletedAt) return &p, nil } diff --git a/pkg/service/adt.go b/pkg/service/adt.go index c64a192..268fb8c 100644 --- a/pkg/service/adt.go +++ b/pkg/service/adt.go @@ -600,10 +600,12 @@ func (s *PlaytesthubServiceServer) ChangeADTBuild(ctx context.Context, req *pb.C } newGame := req.GetAdtGameId() newBuild := req.GetAdtBuildId() - current.ADTGameID = &newGame - current.ADTBuildID = &newBuild - got, err := s.playtest.Update(ctx, current) + // UpdateADTBuild, not the generic Update: the latter's SET whitelist + // omits adt_game_id / adt_build_id, so routing through it would accept + // the change and silently drop it before Postgres (the build-change + // recovery path would no-op). + got, err := s.playtest.UpdateADTBuild(ctx, s.namespace, current.ID, newGame, newBuild) if e := mapPlaytestLookupErr(err, nil, "updating playtest"); e != nil { return nil, e } @@ -616,6 +618,116 @@ func (s *PlaytesthubServiceServer) ChangeADTBuild(ctx context.Context, req *pb.C return &pb.ChangeADTBuildResponse{Playtest: playtestToProto(got)}, nil } +// ADT build-health values persisted to playtest.adt_build_status (M5.C). +const ( + adtBuildStatusOK = "OK" + adtBuildStatusUnavailable = "UNAVAILABLE" + // adtBuildHealthCheckIdent is the ApplicantIdent forwarded to ADT on a + // CheckADTBuild probe so the throwaway issue is distinguishable from a + // real grant in ADT-side audit. + adtBuildHealthCheckIdent = "build-health-check" +) + +// CheckADTBuild probes whether the playtest's current ADT build can still +// mint a download URL (the same adt.Client.IssueDownloadURL call +// ApproveApplicant makes) and persists the result so the detail page can +// surface a gone/undownloadable build without an approval attempt. A +// transient ADT failure (linkage missing / unreachable) surfaces as a +// gRPC error and leaves the stored status untouched. +func (s *PlaytesthubServiceServer) CheckADTBuild(ctx context.Context, req *pb.CheckADTBuildRequest) (*pb.CheckADTBuildResponse, error) { + if _, err := requireActor(ctx); err != nil { + return nil, err + } + if err := s.checkNamespace(req.GetNamespace()); err != nil { + return nil, err + } + id, err := parseReqUUID("playtest_id", req.GetPlaytestId()) + if err != nil { + return nil, err + } + + current, err := s.playtest.GetByID(ctx, s.namespace, id) + if e := mapPlaytestLookupErr(err, playtestSoftDelete(current), "fetching playtest"); e != nil { + return nil, e + } + if current.DistributionModel != distModelADT { + return nil, status.Error(codes.FailedPrecondition, "playtest does not use ADT distribution; build health only applies to ADT playtests") + } + + health, probeErr := s.probeADTBuildHealth(ctx, current) + if probeErr != nil { + return nil, probeErr + } + got, err := s.playtest.SetADTBuildHealth(ctx, s.namespace, current.ID, health, time.Now().UTC()) + if e := mapPlaytestLookupErr(err, nil, "persisting adt build health"); e != nil { + return nil, e + } + return &pb.CheckADTBuildResponse{Playtest: playtestToProto(got), Healthy: health == adtBuildStatusOK}, nil +} + +// probeADTBuildHealth issues a download URL for the playtest's current +// build and classifies the result into a persistable status. Unlike +// resolveADTDownloadURL it ignores adt_fallback_download_url — a fallback +// masks a dead build for approval but does not make the build itself +// healthy, which is exactly what this surface must report. Returns +// ("OK"|"UNAVAILABLE", nil) for determinate outcomes, or a gRPC error for +// transient failures the caller must not persist. +func (s *PlaytesthubServiceServer) probeADTBuildHealth(ctx context.Context, playtest *repo.Playtest) (string, error) { + if s.adtClient == nil { + return "", status.Error(codes.Internal, "ADT client not configured") + } + if playtest.ADTNamespace == nil || playtest.ADTGameID == nil || playtest.ADTBuildID == nil { + return "", status.Error(codes.Internal, "ADT playtest missing identifiers") + } + studio, err := s.resolveStudioNamespace(ctx) + if err != nil { + return "", err + } + _, issueErr := s.adtClient.IssueDownloadURL(ctx, adt.IssueDownloadURLParams{ + StudioNamespace: studio, + ADTNamespace: *playtest.ADTNamespace, + ADTGameID: *playtest.ADTGameID, + ADTBuildID: *playtest.ADTBuildID, + ApplicantIdent: adtBuildHealthCheckIdent, + }) + if health, ok := adtBuildHealthFromIssueErr(issueErr); ok { + return health, nil + } + if errors.Is(issueErr, adt.ErrLinkageMissing) { + return "", status.Error(codes.FailedPrecondition, "adt linkage no longer exists or service token rejected, re-link required") + } + return "", status.Errorf(codes.Unavailable, "calling ADT IssueDownloadURL: %v", issueErr) +} + +// adtBuildHealthFromIssueErr maps an IssueDownloadURL error into a +// persistable adt_build_status. ok=false means the error is transient +// (linkage missing / unreachable) and the last-known status must be left +// in place rather than overwritten. +func adtBuildHealthFromIssueErr(issueErr error) (string, bool) { + if issueErr == nil { + return adtBuildStatusOK, true + } + if errors.Is(issueErr, adt.ErrBuildNotFound) { + return adtBuildStatusUnavailable, true + } + return "", false +} + +// recordADTBuildHealth opportunistically persists build health observed +// while resolving a download URL on the approve / RetryDM paths. Best +// effort: a write failure is logged, never surfaced, so it cannot mask +// the primary approve outcome. Transient ADT errors are skipped so a +// blip does not clobber a previously-good status. +func (s *PlaytesthubServiceServer) recordADTBuildHealth(ctx context.Context, playtest *repo.Playtest, issueErr error) { + health, ok := adtBuildHealthFromIssueErr(issueErr) + if !ok { + return + } + if _, err := s.playtest.SetADTBuildHealth(ctx, s.namespace, playtest.ID, health, time.Now().UTC()); err != nil { + s.loggerOrDefault().Warn("recording adt build health failed", "playtestId", playtest.ID, "err", err) + } +} + // GetADTClientDiagnostics returns the snapshot the bootapp recorded // when it picked between adt.NewHTTPClient and adt.NewMemClient. // Booleans only — the secret-bearing env vars are reported as presence diff --git a/pkg/service/adt_check_build_test.go b/pkg/service/adt_check_build_test.go new file mode 100644 index 0000000..6d54adf --- /dev/null +++ b/pkg/service/adt_check_build_test.go @@ -0,0 +1,141 @@ +package service + +import ( + "testing" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + + pb "github.com/anggorodewanto/playtesthub/pkg/pb/playtesthub/v1" + "github.com/anggorodewanto/playtesthub/pkg/repo" +) + +func TestCheckADTBuild_HealthyPersistsOK(t *testing.T) { + h := newADTTestServer(t) + row := seedADTPlaytestRow(h, "adt-check-ok") + + resp, err := h.svr.CheckADTBuild(authCtx(uuid.New()), &pb.CheckADTBuildRequest{ + Namespace: testNamespace, + PlaytestId: row.ID.String(), + }) + if err != nil { + t.Fatalf("CheckADTBuild: %v", err) + } + if !resp.GetHealthy() { + t.Error("healthy = false, want true") + } + if got := resp.GetPlaytest().GetAdtBuildStatus(); got != adtBuildStatusOK { + t.Errorf("adt_build_status = %q, want %q", got, adtBuildStatusOK) + } + if resp.GetPlaytest().GetAdtBuildCheckedAt() == nil { + t.Error("adt_build_checked_at not set") + } + if row.ADTBuildStatus == nil || *row.ADTBuildStatus != adtBuildStatusOK { + t.Errorf("persisted status = %v, want OK", row.ADTBuildStatus) + } +} + +func TestCheckADTBuild_BuildNotFoundPersistsUnavailable(t *testing.T) { + h := newADTTestServer(t) + h.svr.WithADTClient(buildNotFoundADTClient{}) + row := seedADTPlaytestRow(h, "adt-check-gone") + + resp, err := h.svr.CheckADTBuild(authCtx(uuid.New()), &pb.CheckADTBuildRequest{ + Namespace: testNamespace, + PlaytestId: row.ID.String(), + }) + if err != nil { + t.Fatalf("CheckADTBuild: %v (build-not-found is a determinate result, not an error)", err) + } + if resp.GetHealthy() { + t.Error("healthy = true, want false") + } + if got := resp.GetPlaytest().GetAdtBuildStatus(); got != adtBuildStatusUnavailable { + t.Errorf("adt_build_status = %q, want %q", got, adtBuildStatusUnavailable) + } + if row.ADTBuildStatus == nil || *row.ADTBuildStatus != adtBuildStatusUnavailable { + t.Errorf("persisted status = %v, want UNAVAILABLE", row.ADTBuildStatus) + } +} + +func TestCheckADTBuild_NonADTPlaytest_FailedPrecondition(t *testing.T) { + h := newADTTestServer(t) + row := &repo.Playtest{ + ID: uuid.New(), + Namespace: testNamespace, + Slug: "steam-check", + Title: "T", + DistributionModel: distModelSteamKeys, + Status: statusDraft, + } + h.pt.rows = append(h.pt.rows, row) + + _, err := h.svr.CheckADTBuild(authCtx(uuid.New()), &pb.CheckADTBuildRequest{ + Namespace: testNamespace, + PlaytestId: row.ID.String(), + }) + requireStatus(t, err, codes.FailedPrecondition) + requireMsgContains(t, err, "ADT distribution") +} + +func TestCheckADTBuild_PlaytestNotFound_NotFound(t *testing.T) { + h := newADTTestServer(t) + + _, err := h.svr.CheckADTBuild(authCtx(uuid.New()), &pb.CheckADTBuildRequest{ + Namespace: testNamespace, + PlaytestId: uuid.New().String(), + }) + requireStatus(t, err, codes.NotFound) +} + +// A transient ADT failure (generic 5xx) surfaces as Unavailable and must +// NOT overwrite the stored status — last-known health survives a blip. +func TestCheckADTBuild_TransientError_DoesNotPersist(t *testing.T) { + h := newADTTestServer(t) + h.svr.WithADTClient(&erroringADTClient{}) + row := seedADTPlaytestRow(h, "adt-check-transient") + + _, err := h.svr.CheckADTBuild(authCtx(uuid.New()), &pb.CheckADTBuildRequest{ + Namespace: testNamespace, + PlaytestId: row.ID.String(), + }) + requireStatus(t, err, codes.Unavailable) + if row.ADTBuildStatus != nil { + t.Errorf("status = %v, want nil (transient error must not persist)", row.ADTBuildStatus) + } +} + +// Approve-time persistence (path 3): a build-not-found approve fails AND +// records UNAVAILABLE so the detail page reflects it without a manual check. +func TestApproveApplicant_ADT_BuildNotFound_PersistsUnavailable(t *testing.T) { + h := newADTApprovedHarness(t) + h.svr.WithADTClient(buildNotFoundADTClient{}) + pt := seedADTPlaytest(t, h) + a := seedPendingApplicantADT(t, h.svr, pt.ID) + + _, err := h.svr.ApproveApplicant(authCtx(uuid.New()), &pb.ApproveApplicantRequest{ + Namespace: testNamespace, + ApplicantId: a.ID.String(), + }) + requireStatus(t, err, codes.FailedPrecondition) + requireMsgContains(t, err, "ADT build no longer exists") + if pt.ADTBuildStatus == nil || *pt.ADTBuildStatus != adtBuildStatusUnavailable { + t.Errorf("persisted status = %v, want UNAVAILABLE", pt.ADTBuildStatus) + } +} + +func TestApproveApplicant_ADT_HappyPath_PersistsOK(t *testing.T) { + h := newADTApprovedHarness(t) + pt := seedADTPlaytest(t, h) + a := seedPendingApplicantADT(t, h.svr, pt.ID) + + if _, err := h.svr.ApproveApplicant(authCtx(uuid.New()), &pb.ApproveApplicantRequest{ + Namespace: testNamespace, + ApplicantId: a.ID.String(), + }); err != nil { + t.Fatalf("ApproveApplicant: %v", err) + } + if pt.ADTBuildStatus == nil || *pt.ADTBuildStatus != adtBuildStatusOK { + t.Errorf("persisted status = %v, want OK", pt.ADTBuildStatus) + } +} diff --git a/pkg/service/approve.go b/pkg/service/approve.go index 512ca82..dbc3e8f 100644 --- a/pkg/service/approve.go +++ b/pkg/service/approve.go @@ -170,6 +170,10 @@ func (s *PlaytesthubServiceServer) resolveADTDownloadURL(ctx context.Context, pl ADTBuildID: *playtest.ADTBuildID, ApplicantIdent: applicant.ID.String(), }) + // Record build health from this real issue attempt (M5.C) before any + // fallback masks it — so the detail page reflects a dead ADT build even + // when adt_fallback_download_url is keeping approvals working. + s.recordADTBuildHealth(ctx, playtest, issueErr) if issueErr == nil { return issued.URLs, adtURLSourceIssued, nil } diff --git a/pkg/service/playtest.go b/pkg/service/playtest.go index 3ea39a8..2f48775 100644 --- a/pkg/service/playtest.go +++ b/pkg/service/playtest.go @@ -711,6 +711,13 @@ func playtestToProto(p *repo.Playtest) *pb.Playtest { v := *p.ADTFallbackDownloadURL out.AdtFallbackDownloadUrl = &v } + if p.ADTBuildStatus != nil { + v := *p.ADTBuildStatus + out.AdtBuildStatus = &v + } + if p.ADTBuildCheckedAt != nil { + out.AdtBuildCheckedAt = timestamppb.New(*p.ADTBuildCheckedAt) + } if p.DeletedAt != nil { out.DeletedAt = timestamppb.New(*p.DeletedAt) } diff --git a/pkg/service/playtest_test.go b/pkg/service/playtest_test.go index d82506a..835cdba 100644 --- a/pkg/service/playtest_test.go +++ b/pkg/service/playtest_test.go @@ -128,6 +128,33 @@ func (f *fakePlaytestStore) SetSurveyID(_ context.Context, namespace string, pla return repo.ErrNotFound } +func (f *fakePlaytestStore) UpdateADTBuild(_ context.Context, namespace string, id uuid.UUID, adtGameID, adtBuildID string) (*repo.Playtest, error) { + for _, r := range f.rows { + if r.Namespace == namespace && r.ID == id && r.DeletedAt == nil { + g, b := adtGameID, adtBuildID + r.ADTGameID = &g + r.ADTBuildID = &b + r.UpdatedAt = time.Now() + ret := *r + return &ret, nil + } + } + return nil, repo.ErrNotFound +} + +func (f *fakePlaytestStore) SetADTBuildHealth(_ context.Context, namespace string, id uuid.UUID, status string, checkedAt time.Time) (*repo.Playtest, error) { + for _, r := range f.rows { + if r.Namespace == namespace && r.ID == id && r.DeletedAt == nil { + s, c := status, checkedAt + r.ADTBuildStatus = &s + r.ADTBuildCheckedAt = &c + ret := *r + return &ret, nil + } + } + return nil, repo.ErrNotFound +} + func (f *fakePlaytestStore) ListDueForAutoTransition(_ context.Context, namespace string, now time.Time) ([]*repo.Playtest, error) { out := make([]*repo.Playtest, 0) for _, r := range f.rows { diff --git a/pkg/service/service_test.go b/pkg/service/service_test.go index e73468a..ca39fa9 100644 --- a/pkg/service/service_test.go +++ b/pkg/service/service_test.go @@ -61,6 +61,7 @@ func TestServiceDescriptorMethods(t *testing.T) { "GetADTClientDiagnostics": true, "GetADTDownloadInfo": true, "ChangeADTBuild": true, + "CheckADTBuild": true, // M5.C (participants + announcements) "GetPlaytestParticipants": true, "CreateAnnouncement": true, diff --git a/proto/playtesthub/v1/playtesthub.proto b/proto/playtesthub/v1/playtesthub.proto index 77d9b2b..71cfe71 100644 --- a/proto/playtesthub/v1/playtesthub.proto +++ b/proto/playtesthub/v1/playtesthub.proto @@ -724,6 +724,32 @@ service PlaytesthubService { }; } + // CheckADTBuild probes whether the playtest's current ADT build can still + // mint a download URL — the same adt.Client.IssueDownloadURL call + // ApproveApplicant makes — and persists the result to adt_build_status + + // adt_build_checked_at so the detail page surfaces a gone/undownloadable + // build without an approval attempt. Side effect: ADT mints a throwaway + // URL on success, so this is an explicit on-demand action, not a passive + // read. + rpc CheckADTBuild(CheckADTBuildRequest) returns (CheckADTBuildResponse) { + option (playtesthub.v1.action) = ACTION_UPDATE; + option (playtesthub.v1.resource) = "ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI"; + option (google.api.http) = { + post: "/v1/admin/namespaces/{namespace}/playtests/{playtest_id}/adt/build:check" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Probe + persist an ADT playtest's build download health (admin)" + description: "Issues a download URL for the playtest's current adt_build_id (same call as ApproveApplicant) and persists adt_build_status: 'OK' when a URL was minted, 'UNAVAILABLE' when ADT returns build-not-found. Non-ADT playtest → FailedPrecondition. Linkage missing / ADT unreachable → FailedPrecondition / Unavailable (status not overwritten). Side effect: a throwaway download URL is minted on success." + security: { + security_requirement: { + key: "Bearer" + value: {} + } + } + }; + } + // M5.C — participants surface backing the detail page Participants tab. // Cache-only read; the four ADT telemetry cache columns ship dormant in // M5.C (NULL/zero across the milestone — M6 lights them up). @@ -891,6 +917,14 @@ message Playtest { optional string adt_game_id = 25; optional string adt_build_id = 26; optional string adt_fallback_download_url = 27; + // ADT build health (M5.C, "build gone" surfacing). Last observed result + // of issuing a download URL for adt_build_id: "OK" (a URL was minted), + // "UNAVAILABLE" (ADT returned build-not-found — the same signal that + // fails ApproveApplicant), or unset (never checked / not an ADT playtest). + // Written by ApproveApplicant/RetryDM opportunistically and on demand by + // CheckADTBuild. adt_build_checked_at is when the status was last observed. + optional string adt_build_status = 28; + optional google.protobuf.Timestamp adt_build_checked_at = 29; } // PublicPlaytest is the restricted field set exposed to unauthenticated @@ -1643,6 +1677,19 @@ message ChangeADTBuildResponse { Playtest playtest = 1; } +message CheckADTBuildRequest { + string namespace = 1; + string playtest_id = 2; +} +message CheckADTBuildResponse { + // The updated playtest row, carrying the refreshed adt_build_status + + // adt_build_checked_at. + Playtest playtest = 1; + // healthy = (adt_build_status == "OK") — convenience mirror so the + // caller need not re-parse the string. + bool healthy = 2; +} + message GetADTClientDiagnosticsRequest { string namespace = 1; } diff --git a/scripts/smoke/pth.sh b/scripts/smoke/pth.sh index 2ce6271..8a2cbbe 100755 --- a/scripts/smoke/pth.sh +++ b/scripts/smoke/pth.sh @@ -656,6 +656,9 @@ log "pth adt build list --dry-run prints the request body" log "pth adt build change --dry-run prints the request body" "$PTH_BIN" --namespace smoke adt build change --playtest-id 01234567-89ab-cdef-0123-456789abcdef --game-id game-y --build-id build-002 --dry-run >/dev/null \ || fail "adt build change --dry-run exited non-zero" +log "pth adt build check --dry-run prints the request body" +"$PTH_BIN" --namespace smoke adt build check --playtest-id 01234567-89ab-cdef-0123-456789abcdef --dry-run >/dev/null \ + || fail "adt build check --dry-run exited non-zero" log "pth adt games list --dry-run prints the request body" "$PTH_BIN" --namespace smoke adt games list --linkage-id 01234567-89ab-cdef-0123-456789abcdef --dry-run >/dev/null \ || fail "adt games list --dry-run exited non-zero"