mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-13 04:09:26 +00:00
Both CodeRabbit findings were right. 1. A HUNG PREPARE COULD BLOCK THE GIG FOREVER. `await fetch(...)` only rejects on a network ERROR. A server that accepts the connection and then never answers hangs indefinitely — and the gig would never start. That makes this optimisation the exact thing the PR promises it can never be: the reason you cannot play. The request is now bounded by an AbortController (PREPARE_TIMEOUT_MS, generous because unpacking a setlist is real work — but a CEILING, not a wait). Past it we start the gig and let the first play extract lazily, as it always did. The Play button is restored in a `finally`, so a timeout cannot strand the poster on "Preparing set…" with Play disabled — which would have been the same bug wearing a different hat. 2. THE `songs` BODY WAS UNVALIDATED. A str is iterable: "abc" would have prepared three one-character "songs". And the endpoint unpacks zips, so an arbitrary caller could ask for unbounded work. Now list-only, string entries, blanks dropped, capped at MAX_GIG_SONGS. Tests: the fetch is abortable and the button is re-enabled on EVERY path including the abort; non-list bodies, non-string/blank entries, and an oversized setlist. 50 career tests, JS 5/5, eslint clean.
99 lines
4.8 KiB
JavaScript
99 lines
4.8 KiB
JavaScript
// A gig is a SET, not a run of unrelated songs.
|
|
//
|
|
// Reported from a live gig: the player finished the first song and had to sit
|
|
// through the per-song results popup before the next one would start, and then
|
|
// wait again while that song was extracted from its feedpak zip.
|
|
//
|
|
// This file covers the CORE half — career pre-extracts the whole setlist before
|
|
// the first note. The other half (note_detect must not show its per-song summary
|
|
// inside a gig) lives in the note_detect plugin repo, which is not part of this
|
|
// checkout: plugins/*/ is gitignored here and note_detect ships from
|
|
// feedBack-plugin-notedetect. A test reading it from core would pass on a dev
|
|
// box (where the plugin happens to be bundled) and fail in CI, which is worse
|
|
// than no test.
|
|
//
|
|
// The pre-extraction is tested for REAL behaviour — actually unpacking zips — in
|
|
// tests/plugins/career/test_routes.py. These are the wiring guards around it.
|
|
|
|
'use strict';
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ROOT = path.join(__dirname, '..', '..');
|
|
const CAREER = fs.readFileSync(path.join(ROOT, 'plugins', 'career', 'screen.js'), 'utf8');
|
|
const CAREER_ROUTES = fs.readFileSync(path.join(ROOT, 'plugins', 'career', 'routes.py'), 'utf8');
|
|
|
|
function extractBlock(src, signature) {
|
|
const start = src.indexOf(signature);
|
|
assert.ok(start !== -1, `signature '${signature}' not found`);
|
|
const openBrace = src.indexOf('{', start);
|
|
let depth = 1;
|
|
let i = openBrace + 1;
|
|
while (i < src.length && depth > 0) {
|
|
const ch = src[i];
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') depth--;
|
|
i++;
|
|
}
|
|
assert.ok(depth === 0, `unbalanced braces after '${signature}'`);
|
|
return src.slice(start, i);
|
|
}
|
|
|
|
test('startGig extracts the whole setlist before starting the queue', () => {
|
|
const fn = extractBlock(CAREER, 'async function startGig(');
|
|
const prepIdx = fn.search(/await\s+prepareGigSongs\s*\(/);
|
|
const startIdx = fn.search(/q\.start\s*\(/);
|
|
assert.ok(prepIdx !== -1, 'startGig must pre-extract the set');
|
|
assert.ok(startIdx !== -1, 'q.start not found');
|
|
assert.ok(prepIdx < startIdx,
|
|
'the set must be unpacked BEFORE the queue starts — otherwise the player ' +
|
|
'waits between songs, which is the bug');
|
|
});
|
|
|
|
test('the stage is only borrowed once the set is ready', () => {
|
|
const fn = extractBlock(CAREER, 'async function startGig(');
|
|
const prepIdx = fn.search(/await\s+prepareGigSongs\s*\(/);
|
|
const stageIdx = fn.search(/VENUE_OVERRIDE_KEY/);
|
|
assert.ok(prepIdx < stageIdx,
|
|
'a gig cancelled while unpacking must not leave the venue/viz overwritten');
|
|
assert.match(fn, /_ppGigProposal\s*!==\s*prop/,
|
|
'a proposal dismissed while unpacking must not then start a gig');
|
|
});
|
|
|
|
test('pre-extraction never blocks the gig from starting', () => {
|
|
const fn = extractBlock(CAREER, 'async function prepareGigSongs(');
|
|
assert.match(fn, /catch\s*\(/,
|
|
'a failed prepare must fall through to the old lazy extraction, not abort the gig');
|
|
});
|
|
|
|
test('the prepare route degrades instead of failing', () => {
|
|
assert.match(CAREER_ROUTES, /def prepare_gig/, 'prepare route missing');
|
|
assert.match(CAREER_ROUTES, /context\.get\(\s*["']get_dlc_dir["']\s*\)/,
|
|
'a host without the library resolvers must degrade, not 500 — pre-extraction ' +
|
|
'is an optimisation and can never be why a gig will not start');
|
|
});
|
|
|
|
// ── the prepare must never be able to BLOCK the gig (CodeRabbit, #971) ──────
|
|
//
|
|
// A bare `await fetch(...)` only rejects on a network error. A server that
|
|
// accepts the connection and then never answers hangs forever — and the gig
|
|
// would never start. That would make this optimisation the exact thing it
|
|
// promises never to be: the reason you cannot play.
|
|
|
|
test('the prepare fetch is bounded — a hung server cannot block the gig', () => {
|
|
const fn = extractBlock(CAREER, 'async function prepareGigSongs(');
|
|
assert.match(fn, /AbortController/, 'the request must be abortable');
|
|
assert.match(fn, /setTimeout\([\s\S]{0,40}abort\s*\(\s*\)/,
|
|
'a hung request must be aborted, not awaited forever');
|
|
assert.match(fn, /signal:\s*ctrl\.signal/, 'the signal must actually be passed to fetch');
|
|
assert.match(fn, /clearTimeout/, 'the timer must be cleared on the happy path');
|
|
assert.match(CAREER, /const\s+PREPARE_TIMEOUT_MS\s*=\s*\d+/, 'the ceiling must be named');
|
|
// The button must be restored however we leave — otherwise a timeout strands
|
|
// the poster on "Preparing set…" with Play disabled: unplayable.
|
|
assert.match(fn, /finally\s*\{[\s\S]{0,220}btn\.disabled\s*=\s*false/,
|
|
'the Play button must be re-enabled on EVERY path, including the abort');
|
|
});
|