124 lines
8.1 KiB
JavaScript
124 lines
8.1 KiB
JavaScript
'use strict';
|
|
// Load the complete reachable production helper closure with the established
|
|
// browser/HTTP/vMix I/O fixture. No planner or strength helper is replaced.
|
|
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
|
|
const previous = fs.readFileSync(path.join(__dirname, 'build199_goal_penalty.cjs'), 'utf8');
|
|
const boundary = previous.indexOf('\nconst cases = {};');
|
|
assert.ok(boundary > 0);
|
|
const prefix = previous.slice(0, boundary);
|
|
const fixture = prefix.replace("['hockeyStoreGameControl', 'hockeyScoreCommand'",
|
|
"['hockeyCommitGoalPenalty', 'controlHockeyPenalty', 'updateHockeyPenaltyBoards', 'createHockeyPenaltyDraft', 'hockeyReadyPenaltyCounts', 'hockeyPenaltySnapshot', 'normalizeStoredHockeyEvent', 'hockeyStoreGameControl', 'hockeyScoreCommand'");
|
|
assert.notEqual(fixture, prefix);
|
|
const { harness, flush, clone, assertInfrastructure } = new Function('require', 'process', '__dirname', fixture
|
|
+ '\nreturn { harness, flush, clone, assertInfrastructure };')(require, process, __dirname);
|
|
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
assert.equal(input.case, process.argv[2]);
|
|
|
|
function runtime() {
|
|
const h = harness(input.penalties);
|
|
h.component.props.presets += '|10=10:00|20=20:00';
|
|
h.server.strength = clone(input.strength);
|
|
h.state.data.hockey.game_control.strength = clone(input.strength);
|
|
h.state.hockeyGameControl['game-A'].strength = clone(input.strength);
|
|
return h;
|
|
}
|
|
|
|
async function verifyCycleReset(h, mode, alreadyRebalanced = false) {
|
|
if (!alreadyRebalanced) { await h.c.rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }); await flush(); }
|
|
assert.equal(h.state.hockeyPenaltyAdvantageCycle.hadAdvantage, true, 'The real display/rebalance path must first establish a PP cycle');
|
|
assert.equal(h.state.hockeyPenaltyAdvantageCycle.lastAdvantageSide, 'home');
|
|
if (mode === 'goal') {
|
|
// This commit is the production ACK-side mutation; delivery ordering is
|
|
// independently exercised by BUILD202 goal-out regressions.
|
|
h.c.hockeyCommitGoalPenalty({ componentId: h.component.action_id, eventId: 'minor',
|
|
eventKey: h.component.action_id + ':minor', finished: true, isCurrent: () => true }, { confirmation: 'ack' });
|
|
} else {
|
|
h.timerStep.timer_finish_actions = [{ id: 'full-strength', enabled: true, source: 'away_penalty',
|
|
input: 'FullStrength', overlay: '4', duration_ms: 3000, only_when_side_clear: true }];
|
|
if (mode === 'natural') { h.advance(1000); h.c.updateHockeyPenaltyBoards(h.c.performance.now()); }
|
|
else await h.c.controlHockeyPenalty(h.component, 'minor', mode, '', { awaitVmix: true });
|
|
}
|
|
for (let n = 0; n < 4; n++) { await flush(); await h.state.vmixCommandQueue; await h.state.vmixUrgentCommandQueue; }
|
|
assertInfrastructure(h);
|
|
assert.deepEqual(Array.from(h.board().penalties, row => row.id), ['discipline']);
|
|
assert.equal(h.state.hockeyPenaltyAdvantageCycle.hadAdvantage, false, 'A remaining10-minute data row must not keep the finished strength cycle alive');
|
|
assert.equal(h.state.hockeyPenaltyAdvantageCycle.lastAdvantageSide, '');
|
|
assert.equal(h.state.hockeyPenaltyAdvantageCycle.displayOwnerEventKey, '');
|
|
if (mode === 'finish' || mode === 'natural') {
|
|
assert.ok(h.commands().some(command => command.Function === 'OverlayInput4In' && command.Input === 'FullStrength'),
|
|
'The real finish action must see remaining_total=0 despite the personal discipline row');
|
|
}
|
|
for (const row of input.next_penalties) assert.ok(h.c.createHockeyPenaltyDraft(h.component, row, 'new-cycle-regression'));
|
|
h.server.strength = clone(input.next_strength);
|
|
h.state.data.hockey.game_control.strength = clone(input.next_strength);
|
|
h.state.hockeyGameControl['game-A'].strength = clone(input.next_strength);
|
|
const next = h.c.penaltyDisplayEntriesByTargetSide(h.timerStep);
|
|
assert.equal(next.holdingEqualStrength, false, 'A fresh opposing2+2 pair must not inherit the previous power-play owner');
|
|
assert.equal(next.plateSide, '');
|
|
assert.equal(h.c.currentHockeyPenaltyEntries().length, 3, 'Keep discipline and both new penalties available to the operator');
|
|
assertInfrastructure(h);
|
|
}
|
|
|
|
(async () => {
|
|
const h = runtime();
|
|
const rawIds = () => Array.from(h.c.currentHockeyPenaltyEntries(), row => row.event.id);
|
|
const allIds = input.penalties.map(row => row.id);
|
|
assert.deepEqual(rawIds(), allIds, 'Personal/queued/offset rows remain available for data and operator editing');
|
|
const counts = h.c.hockeyReadyPenaltyCounts();
|
|
assert.deepEqual([counts.home, counts.away], input.counts, 'Browser ready counts must agree with the real backend strength');
|
|
const strengthIds = Array.from(h.c.sortedPenaltyEntries('', { includePrepared: true }), row => row.event.id);
|
|
assert.equal(strengthIds.length, input.counts[0] + input.counts[1]);
|
|
const goal = { side: input.plan.side, phase: '2', kind: 'normal', strength: clone(input.strength) };
|
|
const planned = h.c.hockeyGoalPenaltyPlan(goal);
|
|
assert.equal(planned.reason, input.plan.reason, 'Goal resolution uses the same strength, with separate releasable-minor eligibility');
|
|
assert.equal(planned.entry?.event.id || null, input.plan.entry || null);
|
|
assert.deepEqual(rawIds(), allIds, 'Planning one eligible penalty must not mutate any row');
|
|
if (input.case === 'equal_minor_choice') {
|
|
h.board().selectedEventId = 'second';
|
|
assert.equal(h.c.hockeyGoalPenaltyPlan(goal).entry?.event.id, 'second');
|
|
}
|
|
if (input.case === 'shorthanded_and_empty_net') {
|
|
h.state.data.hockey.game_control.flags.away_empty_net = true;
|
|
assert.equal(h.c.hockeyGoalPenaltyPlan(goal).reason, 'not_powerplay', 'Pulling a goalkeeper does not turn a shorthanded goal into a PP goal');
|
|
assert.equal(h.c.hockeyGoalPenaltyPlan({ ...goal, side: 'away', kind: 'penalty_shot' }).reason, 'penalty_shot_goal');
|
|
}
|
|
if (input.case === 'major_and_minor') {
|
|
const major = h.board().penalties.find(row => row.id === 'major');
|
|
major.preset = '5';
|
|
assert.equal(h.c.hockeyGoalPenaltyPlan(goal).entry?.event.id, 'minor', 'Both5 and5+20 are strength-affecting but non-releasable');
|
|
}
|
|
if (input.case === 'ordinary_opposing_pair') {
|
|
const plan = h.c.penaltyDisplayEntriesByTargetSide(h.timerStep);
|
|
assert.equal(plan.plateSide, 'away');
|
|
assert.equal(plan.away[0]?.event.id, 'left-two');
|
|
assert.equal(plan.holdingEqualStrength, true, 'Keep the configured4on4 phase and longer opposing tail owner');
|
|
}
|
|
const snapshots = [];
|
|
if (input.case === 'metadata_roundtrip') {
|
|
h.board().penalties = h.board().penalties.map(row => h.c.normalizeStoredHockeyEvent(h.component, row));
|
|
snapshots.push(...h.board().penalties.map(row => clone(h.c.hockeyPenaltySnapshot(row))));
|
|
h.board().penalties = snapshots.map(row => h.c.normalizeStoredHockeyEvent(h.component, row));
|
|
assert.deepEqual([h.c.hockeyReadyPenaltyCounts().home, h.c.hockeyReadyPenaltyCounts().away], input.counts,
|
|
'Strength metadata survives normalizer, snapshot, JSON persistence and hydration');
|
|
assert.equal(h.board().penalties.find(row => row.id === 'offset').offsetting, true);
|
|
assert.equal(h.board().penalties.find(row => row.id === 'personal').affectsStrength, false);
|
|
}
|
|
await h.c.rebalanceVmixPenaltyTargets({ force: true, hideUnused: true });
|
|
await flush(); assertInfrastructure(h);
|
|
for (const assignment of h.state.vmixPenaltyTargetAssignments.values()) {
|
|
assert.ok(strengthIds.some(id => assignment.eventKey.endsWith(':' + id)),
|
|
'Physical penalty clocks may only be owned by a current strength-affecting penalty');
|
|
}
|
|
if (input.counts[0] + input.counts[1] === 0) {
|
|
assert.equal(h.commands().length, 0, 'A personal/offset penalty alone must not create or start an on-air penalty clock');
|
|
}
|
|
assert.deepEqual(rawIds(), allIds);
|
|
if (input.case === 'goal_cycle_reset_with_discipline') await verifyCycleReset(h, 'goal', true);
|
|
if (input.case === 'manual_expiry_cycle_reset_with_discipline') {
|
|
await verifyCycleReset(h, 'remove', true);
|
|
await verifyCycleReset(runtime(), 'finish');
|
|
await verifyCycleReset(runtime(), 'natural');
|
|
}
|
|
process.stdout.write(JSON.stringify({ passed: input.case, snapshots }));
|
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|