fix(19): address koala P2 review — sync enforcement test, fail on unassigned panels

P2.3: Add tests/chunk-assignment.test.mjs — cross-checks *_PANEL_FILES
sets against src/components/*.ts at test time. Catches drift, duplicates,
ghost entries, and verifies the silent catch-all is gone.

P2.4: Replace `endsWith('Panel')` silent catch-all with a build error.
New panels must be explicitly assigned to a chunk group or the build fails.

Also: assign 11 previously-unassigned panels to correct chunk sets,
remove 2 ghost entries (CommoditiesPanel, HeatmapPanel are sub-exports
of MarketPanel.ts, not standalone files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sebastien Melki
2026-04-15 20:24:39 +03:00
parent 07a4930222
commit 41c62686be
2 changed files with 94 additions and 4 deletions

View File

@@ -0,0 +1,77 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const viteConfigSource = readFileSync(resolve(__dirname, '../vite.config.ts'), 'utf-8');
/**
* Extract all quoted string entries from a `new Set([...])` block.
*/
function extractSetEntries(varName) {
const re = new RegExp(`const ${varName}\\s*=\\s*new Set\\(\\[([\\s\\S]*?)\\]\\)`, 'm');
const match = viteConfigSource.match(re);
if (!match) return new Set();
// Match only quoted strings — ignores comments and whitespace
const entries = match[1].match(/'([^']+)'/g) || [];
return new Set(entries.map(s => s.replace(/^'|'$/g, '')));
}
const CORE = extractSetEntries('CORE_PANEL_FILES');
const HAPPY = extractSetEntries('HAPPY_PANEL_FILES');
const FINANCE = extractSetEntries('FINANCE_PANEL_FILES');
const FULL = extractSetEntries('FULL_PANEL_FILES');
const TECH = extractSetEntries('TECH_PANEL_FILES');
const ALL_ASSIGNED = new Set([...CORE, ...HAPPY, ...FINANCE, ...FULL, ...TECH]);
const componentFiles = readdirSync(resolve(__dirname, '../src/components'))
.filter(f => f.endsWith('.ts') && !f.includes('.test.'))
.map(f => f.replace(/\.ts$/, ''));
const panelFiles = componentFiles.filter(f => f.endsWith('Panel') || f === 'Panel');
describe('chunk assignment sync enforcement', () => {
it('every *Panel.ts component is assigned to exactly one chunk set', () => {
const unassigned = panelFiles.filter(f => !ALL_ASSIGNED.has(f));
assert.deepEqual(
unassigned,
[],
`Unassigned panel files (add to a *_PANEL_FILES set in vite.config.ts): ${unassigned.join(', ')}`
);
});
it('no panel appears in multiple chunk sets', () => {
const sets = { CORE, HAPPY, FINANCE, FULL, TECH };
const dupes = [];
for (const file of ALL_ASSIGNED) {
const inSets = Object.entries(sets)
.filter(([, s]) => s.has(file))
.map(([name]) => name);
if (inSets.length > 1) {
dupes.push(`${file}${inSets.join(', ')}`);
}
}
assert.deepEqual(dupes, [], `Panels in multiple chunk sets:\n${dupes.join('\n')}`);
});
it('chunk sets do not reference non-existent component files', () => {
const fileSet = new Set(componentFiles);
const ghosts = [...ALL_ASSIGNED].filter(f => !fileSet.has(f));
assert.deepEqual(
ghosts,
[],
`Chunk sets reference files that don't exist in src/components/: ${ghosts.join(', ')}`
);
});
it('vite.config.ts has no silent Panel catch-all fallback', () => {
assert.doesNotMatch(
viteConfigSource,
/if\s*\(fileName\.endsWith\('Panel'\).*\)\s*return\s*'core-panels'/,
'The silent catch-all that routes unassigned panels to core-panels must not exist. ' +
'Use the throw-on-unassigned pattern instead.'
);
});
});

View File

@@ -27,10 +27,14 @@ const CORE_PANEL_FILES = new Set([
'EconomicPanel', 'MacroSignalsPanel', 'ETFFlowsPanel', 'StablecoinPanel',
'WorldClockPanel', 'AirlineIntelPanel',
// Shared across full + finance + commodity (3 variants)
'EnergyComplexPanel', 'OilInventoriesPanel', 'CommoditiesPanel',
'HeatmapPanel', 'TradePolicyPanel', 'SupplyChainPanel',
// Note: CommoditiesPanel and HeatmapPanel are sub-exports of MarketPanel.ts, not standalone files
'EnergyComplexPanel', 'OilInventoriesPanel',
'TradePolicyPanel', 'SupplyChainPanel',
'SanctionsPressurePanel', 'GulfEconomiesPanel', 'ConsumerPricesPanel',
'LiquidityShiftsPanel', 'GoldIntelligencePanel',
// Programmatic / multi-variant panels (used dynamically or across 2+ variants)
'CustomWidgetPanel', 'McpDataPanel', 'CountryBriefPanel',
'CountryDeepDivePanel', 'TechHubsPanel', 'RegulationPanel',
]);
const HAPPY_PANEL_FILES = new Set([
@@ -60,6 +64,9 @@ const FULL_PANEL_FILES = new Set([
'MarketImplicationsPanel', 'SatelliteFiresPanel', 'HormuzPanel',
'UcdpEventsPanel', 'ClimateNewsPanel', 'DiseaseOutbreaksPanel',
'SocialVelocityPanel', 'EnergyCrisisPanel',
// Full-only panels
'ForecastPanel', 'ChatAnalystPanel', 'CrossSourceSignalsPanel',
'DeductionPanel', 'GeoHubsPanel',
]);
const TECH_PANEL_FILES = new Set([
@@ -907,8 +914,14 @@ export default defineConfig(({ mode }) => {
if (FINANCE_PANEL_FILES.has(fileName)) return 'finance-panels';
if (FULL_PANEL_FILES.has(fileName)) return 'full-panels';
if (TECH_PANEL_FILES.has(fileName)) return 'tech-panels';
// Unassigned Panel files default to core-panels to avoid orphan chunks
if (fileName.endsWith('Panel') || fileName === 'Panel') return 'core-panels';
// Fail on unassigned panel files — add new panels to the correct set above
if (fileName.endsWith('Panel') || fileName === 'Panel') {
throw new Error(
`[manualChunks] Unassigned panel component: ${fileName}. ` +
`Add it to CORE_PANEL_FILES, FULL_PANEL_FILES, HAPPY_PANEL_FILES, ` +
`FINANCE_PANEL_FILES, or TECH_PANEL_FILES in vite.config.ts.`
);
}
}
}
// Give lazy-loaded locale chunks a recognizable prefix so the