Merge pull request #197 from davidmonterocrespo24/fix/components-metadata-stale-and-vitest-worker-hang

fix: regenerate components-metadata + plug vitest worker leak
This commit is contained in:
David Montero Crespo 2026-05-19 10:52:28 -03:00 committed by GitHub
commit be55c97cce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 11 deletions

View File

@ -518,7 +518,7 @@
"description": "Single-channel optocoupler. CTR 80600% (typ. 100%). Higher drive than 4N25, commonly used for MCU-to-mains isolation."
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n POWER-SUPPLY\n </text>\n </svg>",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"10\" y=\"14\" width=\"44\" height=\"30\" fill=\"#2a3548\" rx=\"3\" stroke=\"#4a5878\"/>\n <text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"9\" font-family=\"monospace\" fill=\"#4ade80\" font-weight=\"bold\">5.00V</text>\n <text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#fbbf24\">1.00A</text>\n <circle cx=\"18\" cy=\"52\" r=\"3\" fill=\"#dc2626\"/>\n <circle cx=\"46\" cy=\"52\" r=\"3\" fill=\"#0f172a\" stroke=\"#64748b\"/>\n <text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"6\" fill=\"#9d9d9d\">PSU</text>\n </svg>",
"tags": [
"power-supply",
"regulated",

View File

@ -20,7 +20,26 @@ import {
} from '../simulation/spice/MixedModeScheduler';
import { FakeSolverAdapter } from '../simulation/spice/adapters/FakeSolverAdapter';
// Tracks unsubscribe handles returned by `service.start()` so the
// store subscription is released after every test. Without this, the
// listener pins the simStore (and via closure the service + scheduler)
// in memory, and vitest's forks pool can't terminate cleanly when the
// suite finishes — manifesting as "Worker exited unexpectedly /
// Timeout terminating forks worker" on CI. The hang doesn't surface
// any failed assertion; everything passes, but the worker process
// never exits.
const _activeUnsubs: Array<() => void> = [];
function startTracked(service: { start: () => () => void }): () => void {
const unsub = service.start();
_activeUnsubs.push(unsub);
return unsub;
}
afterEach(() => {
for (const unsub of _activeUnsubs.splice(0)) {
try { unsub(); } catch { /* ignore */ }
}
__resetMixedModeScheduler();
});
@ -98,7 +117,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({ '5V': { type: 'digital', v: 5 } }) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 20));
expect(fake.calls.loadCircuit.length).toBe(1);
@ -121,7 +140,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({ '5V': { type: 'digital', v: 5 } }) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 20));
const snap = elec.snapshots[0];
@ -139,7 +158,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({ '5V': { type: 'digital', v: 5 } }) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 10));
expect(fake.calls.solve.length).toBe(1);
@ -159,7 +178,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 10));
sim.set({}); // same arrays — should NOT trigger a re-solve
await new Promise((r) => setTimeout(r, 10));
@ -180,7 +199,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
// While initial solve runs, fire 3 store changes — should coalesce
// into 1 trailing solve.
sim.set({ components: [{ id: 'a', metadataId: 'resistor', properties: {} }] });
@ -218,7 +237,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 20));
const snap = elec.snapshots[0];
@ -241,7 +260,7 @@ describe('CircuitSimulationService — orchestration', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 10));
// The FakeSolverAdapter returns empty warnings, so the snapshot
// also has empty warnings — but the field exists.
@ -294,7 +313,7 @@ describe('handleMcuEdge (Phase 1c D1)', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 20));
const initialSolves = fake.calls.solve.length;
const initialSnapshots = elec.snapshots.length;
@ -323,7 +342,7 @@ describe('handleMcuEdge (Phase 1c D1)', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
// While initial solve is running, fire an edge.
void service.handleMcuEdge('uno', '9', true, 5);
await new Promise((r) => setTimeout(r, 100));
@ -371,7 +390,7 @@ describe('CircuitSimulationService — error handling', () => {
getMixedModeScheduler() as unknown as MixedModeSchedulerPort,
{ collectBoardPinStates: () => ({}) },
);
service.start();
startTracked(service);
await new Promise((r) => setTimeout(r, 10));
expect(warn).toHaveBeenCalled();
expect(elec.snapshots.length).toBe(0); // no publish on failure