mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
* feat(career): career plugin — stars from song_stats, venue tiers, pack downloads (career mode PR2) Bundled plugin: per-song stars from best_accuracy (60/75/85% → 1/2/3★), cumulative stars unlock bar → club → arena (data-driven venues.json). Venue packs (UE-rendered crowd loops) download on demand to CONFIG_DIR/plugin_uploads/career/ on a background thread with sha256 + zip-slip validation, served via FileResponse. Career screen (promoted sidebar entry) shows progress and pushes the active venue's manifest into the crowd video layer (v3VenueCrowd, PR1) — degrades cleanly when either side is absent. Pack URLs land in venues.json in PR3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): keep manifest cleanup path alive on delete; badge only for installed venues Codex preflight: nulling _appliedManifestVenue on delete skipped pushCrowdManifest's setManifest(null) cleanup, leaving the crowd layer on a deleted pack; and the 'playing here' badge showed for an override venue whose pack was removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): generation-guard in-flight manifest fetches Codex preflight: a manifest fetch resolving after a newer refresh (pack deleted, venue switched) could re-apply a stale pack over the user's newer selection — fetches now carry a generation token and bail when superseded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): exclude orphaned song_stats from star totals Codex preflight: scans hide rather than delete stats of removed songs, so stars now apply the same existing-song filter other stats surfaces use (filename IN (SELECT filename FROM songs)). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(career): 50/150 star thresholds + star collection overview Byron's progression tuning: club at 50★, arena at 150★. /state now returns star_detail rows (title/artist joined from the library, stars, best accuracy, next-star threshold) sorted closest-to-next-star first, and the career screen renders a collection panel: tier summary plus a per-song list with a 'N% to next star' practice hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(career): venue select/unselect UX, intro manifest support, fullmatch guards - 'Play here' now also defaults the visualization to Venue (remembering the prior viz); active venues show 'Leave venue' which restores it and sets the '__none__' override so no installed venue silently reapplies. - Pack manifests may ship an intro block (flyover video + ambience mp3); files validate like loops/stingers, .mp3 added to the serving whitelist. - Codex preflight: whitelist regexes use fullmatch (trailing-newline names could validate but 500 on serving). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): let pushCrowdManifest clear the manifest on Leave venue Codex preflight: nulling _appliedManifestVenue before refresh skipped the setManifest(null) cleanup branch, leaving the crowd playing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): refresh tailwind output --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
51 lines
2.2 KiB
JavaScript
51 lines
2.2 KiB
JavaScript
'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 PLUGIN_DIR = path.join(ROOT, 'plugins', 'career');
|
|
const SHELL_JS = path.join(ROOT, 'static', 'v3', 'shell.js');
|
|
|
|
test('career plugin manifest is complete and bundled', () => {
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, 'plugin.json'), 'utf8'));
|
|
assert.equal(manifest.id, 'career');
|
|
assert.equal(manifest.bundled, true);
|
|
assert.equal(manifest.screen, 'screen.html');
|
|
assert.equal(manifest.script, 'screen.js');
|
|
assert.equal(manifest.routes, 'routes.py');
|
|
for (const f of ['screen.html', 'screen.js', 'routes.py', 'venues.json', manifest.styles]) {
|
|
assert.ok(fs.existsSync(path.join(PLUGIN_DIR, f)), `${f} missing`);
|
|
}
|
|
});
|
|
|
|
test('venues.json defines the 3 ascending tiers with star thresholds', () => {
|
|
const content = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, 'venues.json'), 'utf8'));
|
|
assert.deepEqual(content.star_accuracy_thresholds, [0.6, 0.75, 0.85]);
|
|
const venues = content.venues;
|
|
assert.deepEqual(venues.map((v) => v.id), ['bar', 'club', 'arena']);
|
|
assert.equal(venues[0].star_threshold, 0, 'bar must always be unlocked');
|
|
for (let i = 1; i < venues.length; i++) {
|
|
assert.ok(venues[i].star_threshold > venues[i - 1].star_threshold,
|
|
'thresholds must ascend');
|
|
}
|
|
});
|
|
|
|
test('shell promotes the career plugin into the sidebar', () => {
|
|
const src = fs.readFileSync(SHELL_JS, 'utf8');
|
|
assert.match(src, /key: 'career',\s*screen: 'plugin-career'/);
|
|
assert.match(src, /navKey: 'career',\s*pluginId: 'career',\s*slotId: 'v3-nav-career'/);
|
|
});
|
|
|
|
test('career screen pushes the crowd manifest with a base URL', () => {
|
|
const src = fs.readFileSync(path.join(PLUGIN_DIR, 'screen.js'), 'utf8');
|
|
assert.match(src, /v3VenueCrowd/);
|
|
assert.match(src, /setManifest\(manifest\)/);
|
|
assert.match(src, /manifest\.base = /);
|
|
assert.match(src, /feedBack-career-venue/);
|
|
// Degrades without the crowd layer (PR1 not merged / older desktop).
|
|
assert.match(src, /typeof crowd\.setManifest !== 'function'\) return/);
|
|
});
|