feat: wire bark battle platform loop
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createBarkBattleDraft, publishBarkBattleWork } from './barkBattleCreationClient';
|
||||
|
||||
const requestJsonMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
describe('barkBattleCreationClient', () => {
|
||||
afterEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
it('creates a lightweight draft through creation API', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ draftId: 'draft-1' });
|
||||
|
||||
await createBarkBattleDraft({
|
||||
title: '周末狗狗杯',
|
||||
description: '',
|
||||
themePreset: 'neon-park',
|
||||
playerDogSkinPreset: 'shiba',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'hard',
|
||||
leaderboardEnabled: true,
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/bark-battle/drafts',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: '周末狗狗杯',
|
||||
description: '',
|
||||
themePreset: 'neon-park',
|
||||
playerDogSkinPreset: 'shiba',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'hard',
|
||||
leaderboardEnabled: true,
|
||||
}),
|
||||
}),
|
||||
'创建汪汪声浪大作战草稿失败',
|
||||
expect.objectContaining({ retry: expect.objectContaining({ retryUnsafeMethods: true }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('publishes a draft and returns stable work config', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ workId: 'work-1' });
|
||||
|
||||
await publishBarkBattleWork({ draftId: 'draft-1', workId: 'work-1' });
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/bark-battle/works/publish',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ draftId: 'draft-1', workId: 'work-1' }),
|
||||
}),
|
||||
'发布汪汪声浪大作战作品失败',
|
||||
expect.objectContaining({ retry: expect.objectContaining({ retryUnsafeMethods: true }) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import type {
|
||||
BarkBattleDraftConfig,
|
||||
BarkBattleDraftCreateRequest,
|
||||
BarkBattlePublishedConfig,
|
||||
BarkBattleWorkPublishRequest,
|
||||
} from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
|
||||
const BARK_BATTLE_CREATION_API_BASE = '/api/creation/bark-battle';
|
||||
|
||||
const BARK_BATTLE_CREATION_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
export type BarkBattleCreationRequestOptions = Pick<
|
||||
ApiRequestOptions,
|
||||
| 'authImpact'
|
||||
| 'skipRefresh'
|
||||
| 'notifyAuthStateChange'
|
||||
| 'clearAuthOnUnauthorized'
|
||||
>;
|
||||
|
||||
export function createBarkBattleDraft(
|
||||
payload: BarkBattleDraftCreateRequest,
|
||||
options: BarkBattleCreationRequestOptions = {},
|
||||
) {
|
||||
return requestJson<BarkBattleDraftConfig>(
|
||||
`${BARK_BATTLE_CREATION_API_BASE}/drafts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建汪汪声浪大作战草稿失败',
|
||||
{
|
||||
retry: BARK_BATTLE_CREATION_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function publishBarkBattleWork(
|
||||
payload: BarkBattleWorkPublishRequest,
|
||||
options: BarkBattleCreationRequestOptions = {},
|
||||
) {
|
||||
return requestJson<BarkBattlePublishedConfig>(
|
||||
`${BARK_BATTLE_CREATION_API_BASE}/works/publish`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'发布汪汪声浪大作战作品失败',
|
||||
{
|
||||
retry: BARK_BATTLE_CREATION_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const barkBattleCreationClient = {
|
||||
createDraft: createBarkBattleDraft,
|
||||
publish: publishBarkBattleWork,
|
||||
};
|
||||
6
src/services/bark-battle-creation/index.ts
Normal file
6
src/services/bark-battle-creation/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
barkBattleCreationClient,
|
||||
type BarkBattleCreationRequestOptions,
|
||||
createBarkBattleDraft,
|
||||
publishBarkBattleWork,
|
||||
} from './barkBattleCreationClient';
|
||||
@@ -0,0 +1,88 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
finishBarkBattleRun,
|
||||
getBarkBattleRuntimeConfig,
|
||||
startBarkBattleRun,
|
||||
} from './barkBattleRuntimeClient';
|
||||
|
||||
const requestJsonMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
describe('barkBattleRuntimeClient', () => {
|
||||
afterEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
it('reads runtime config from stable work route', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ workId: 'work-1' });
|
||||
|
||||
await getBarkBattleRuntimeConfig('work/1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/bark-battle/works/work%2F1/config',
|
||||
{ method: 'GET' },
|
||||
'读取汪汪声浪大作战配置失败',
|
||||
expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1 }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('starts a formal run with workId in body', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ runId: 'run-1' });
|
||||
|
||||
await startBarkBattleRun('work-1', { sourceRoute: '/play/work-1' });
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/bark-battle/works/work-1/runs',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sourceRoute: '/play/work-1', workId: 'work-1' }),
|
||||
}),
|
||||
'启动汪汪声浪大作战正式局失败',
|
||||
expect.objectContaining({ retry: expect.objectContaining({ retryUnsafeMethods: true }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('finishes a run using derived metrics only', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ status: 'accepted' });
|
||||
|
||||
await finishBarkBattleRun('run-1', {
|
||||
runId: 'run-1',
|
||||
runToken: 'token-1',
|
||||
workId: 'work-1',
|
||||
configVersion: 1,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
clientStartedAt: '2026-05-13T00:00:00Z',
|
||||
clientFinishedAt: '2026-05-13T00:00:30Z',
|
||||
durationMs: 30000,
|
||||
derivedMetrics: {
|
||||
triggerCount: 12,
|
||||
maxVolume: 0.82,
|
||||
averageVolume: 0.36,
|
||||
finalEnergy: 58,
|
||||
comboMax: 4,
|
||||
},
|
||||
clientResult: 'player_win',
|
||||
});
|
||||
|
||||
const [, init] = requestJsonMock.mock.calls[0];
|
||||
expect(requestJsonMock.mock.calls[0][0]).toBe(
|
||||
'/api/runtime/bark-battle/runs/run-1/finish',
|
||||
);
|
||||
expect(JSON.parse(init.body)).toEqual(
|
||||
expect.objectContaining({
|
||||
runId: 'run-1',
|
||||
runToken: 'token-1',
|
||||
derivedMetrics: expect.objectContaining({ finalEnergy: 58 }),
|
||||
}),
|
||||
);
|
||||
expect(init.body).not.toContain('audio');
|
||||
expect(init.body).not.toContain('waveform');
|
||||
expect(init.body).not.toContain('pcm');
|
||||
});
|
||||
});
|
||||
121
src/services/bark-battle-runtime/barkBattleRuntimeClient.ts
Normal file
121
src/services/bark-battle-runtime/barkBattleRuntimeClient.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
BarkBattleFinishResponse,
|
||||
BarkBattleRunFinishRequest,
|
||||
BarkBattleRunStartRequest,
|
||||
BarkBattleRunStartResponse,
|
||||
BarkBattleRuntimeConfig,
|
||||
} from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
|
||||
const BARK_BATTLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
|
||||
const BARK_BATTLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
export type BarkBattleRuntimeRequestOptions = Pick<
|
||||
ApiRequestOptions,
|
||||
| 'authImpact'
|
||||
| 'skipRefresh'
|
||||
| 'notifyAuthStateChange'
|
||||
| 'clearAuthOnUnauthorized'
|
||||
>;
|
||||
|
||||
export function getBarkBattleRuntimeConfig(
|
||||
workId: string,
|
||||
options: BarkBattleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<BarkBattleRuntimeConfig>(
|
||||
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/config`,
|
||||
{ method: 'GET' },
|
||||
'读取汪汪声浪大作战配置失败',
|
||||
{
|
||||
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function startBarkBattleRun(
|
||||
workId: string,
|
||||
payload: Partial<BarkBattleRunStartRequest> = {},
|
||||
options: BarkBattleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<BarkBattleRunStartResponse>(
|
||||
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
workId: payload.workId ?? workId,
|
||||
}),
|
||||
},
|
||||
'启动汪汪声浪大作战正式局失败',
|
||||
{
|
||||
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getBarkBattleRun(
|
||||
runId: string,
|
||||
options: BarkBattleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<unknown>(
|
||||
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取汪汪声浪大作战单局失败',
|
||||
{
|
||||
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function finishBarkBattleRun(
|
||||
runId: string,
|
||||
payload: BarkBattleRunFinishRequest,
|
||||
options: BarkBattleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<BarkBattleFinishResponse>(
|
||||
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}/finish`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
}),
|
||||
},
|
||||
'提交汪汪声浪大作战成绩失败',
|
||||
{
|
||||
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
7
src/services/bark-battle-runtime/index.ts
Normal file
7
src/services/bark-battle-runtime/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
type BarkBattleRuntimeRequestOptions,
|
||||
finishBarkBattleRun,
|
||||
getBarkBattleRun,
|
||||
getBarkBattleRuntimeConfig,
|
||||
startBarkBattleRun,
|
||||
} from './barkBattleRuntimeClient';
|
||||
Reference in New Issue
Block a user