/**
* Сценарий synth — спек-синтез: агент без слуха описывает звуки параметрами
* (scene:synthesize → playSpec), факты запуска читаем из window.__gameAudioLog.
* 1) удар (hit): запуск в логе с заданным ключом и громкостью;
* 2) звон (chime): другой ключ, кэш — компиляция не мешает повтору;
* 3) детерминизм: тот же спек дважды — оба запуска в логе;
* 4) негодный спек (нет dur / неизвестный kind) — команда возвращает null;
* 5) гул (hum) не глушит амбиент области (разные голоса).
* Запуск: node tools/agent.mjs run tools/checks/synth.mjs
*/
import { withChecks } from '../lib.mjs';
export default async function ({ pretty }) {
return withChecks(
'synth',
async (t) => {
const { c } = t;
/**
* Команда моста с ожиданием появления ключа в логе
* (синтез асинхронный): считаем запуски ключа до и после.
*/
const synthAndWait = async (spec, { key = 'agent/synth', volume, tries = 12 } = {}) => {
const count = async () =>
(await t.readAudioLog()).filter((e) => e.key === key).length;
const before = await count();
const res = await t.ctx.agent.command('scene:synthesize', {
spec,
key,
...(volume !== undefined ? { volume } : {})
});
const log = await t.waitAudioLog((l) => l.filter((e) => e.key === key).length > before, tries);
return { res, log };
};
await c.run('новая игра: спек-синтез недоступен в прод-сборке с внятной ошибкой', async () => {
await t.boot();
c.expect(Array.isArray(await t.readAudioLog()), 'DEV-лог аудио недоступен (прод-сборка?)');
return null;
});
await c.run('удар по спеку: запуск в логе с заданным ключом и громкостью', async () => {
const { res, log } = await synthAndWait(
{ kind: 'hit', dur: 0.3, low: 600, high: 3000, seed: 7 },
{ key: 'agent/hit-test', volume: 0.5 }
);
c.expect(res === true, 'scene:synthesize не принят мостом', res);
const hits = log.filter((e) => e.key === 'agent/hit-test');
c.expect(hits.length > 0, 'удар по спеку не сыграл', log);
c.expect(Math.abs(hits.at(-1).volume - 0.5) < 0.01, 'громкость не дошла до голоса', hits.at(-1));
return null;
});
await c.run('звон по спеку: другой ключ играет параллельно', async () => {
const { res, log } = await synthAndWait(
{ kind: 'chime', dur: 0.6, freq: 880 },
{ key: 'agent/chime-test' }
);
c.expect(res === true, 'scene:synthesize не принят мостом', res);
c.expect(log.some((e) => e.key === 'agent/chime-test'), 'звон по спеку не сыграл', log);
c.expect(
log.some((e) => e.key === 'agent/hit-test'),
'предыдущий голос пропал из лога (лог мал?)',
log
);
return null;
});
await c.run('детерминизм: тот же спек дважды — оба запуска в логе', async () => {
const spec = { kind: 'scrape', dur: 0.5, low: 80, high: 400, tone: 60, seed: 3 };
const first = await synthAndWait(spec, { key: 'agent/scrape-test' });
const second = await synthAndWait(spec, { key: 'agent/scrape-test' });
c.expect(first.res === true && second.res === true, 'команда не принята', [first.res, second.res]);
const plays = second.log.filter((e) => e.key === 'agent/scrape-test');
c.expect(plays.length >= 2, 'повторный синтез не сыграл', plays);
return null;
});
await c.run('негодный спек: без dur и с неизвестным kind — null', async () => {
const noDur = await t.ctx.agent.command('scene:synthesize', { spec: { kind: 'hit' } });
const badKind = await t.ctx.agent.command('scene:synthesize', { spec: { kind: 'thunder', dur: 1 } });
const noSpec = await t.ctx.agent.command('scene:synthesize', {});
c.expect(noDur === null, 'спек без dur должен вернуть null', noDur);
c.expect(badKind === null, 'неизвестный kind должен вернуть null', badKind);
c.expect(noSpec === null, 'вызов без спека должен вернуть null', noSpec);
return null;
});
await c.run('гул (hum) не глушит амбиент области', async () => {
const { res, log } = await synthAndWait(
{ kind: 'hum', dur: 2, tone: 55, loop: true },
{ key: 'agent/hum-test' }
);
c.expect(res === true, 'scene:synthesize не принят мостом', res);
c.expect(log.some((e) => e.key === 'agent/hum-test'), 'гул по спеку не сыграл', log);
c.expect(log.some((e) => e.key === 'ambience/meadows'), 'амбиент области пропал', log);
return null;
});
},
{ pretty }
);
}