feat: wire bark battle platform loop
Some checks failed
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-05-14 18:20:46 +08:00
parent 8c6ec9e6e4
commit 1d7ef7e4b6
73 changed files with 7933 additions and 107 deletions

View File

@@ -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 }) }),
);
});
});

View File

@@ -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,
};

View File

@@ -0,0 +1,6 @@
export {
barkBattleCreationClient,
type BarkBattleCreationRequestOptions,
createBarkBattleDraft,
publishBarkBattleWork,
} from './barkBattleCreationClient';

View File

@@ -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');
});
});

View 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,
},
);
}

View File

@@ -0,0 +1,7 @@
export {
type BarkBattleRuntimeRequestOptions,
finishBarkBattleRun,
getBarkBattleRun,
getBarkBattleRuntimeConfig,
startBarkBattleRun,
} from './barkBattleRuntimeClient';