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,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';