velxio/frontend/src/__tests__/library-manifest.test.ts

141 lines
4.9 KiB
TypeScript

/**
* Library-manifest helpers (utils/libraryManifest.ts).
*
* These pure functions drive the 2026-08 manifest migration:
* - mergeSuggestedLibraries folds the backend's post-build report
* (`manifest_suggested_libraries`) into a board's declared manifest —
* conservatively: single-candidate entries only, normalized dedup,
* null when nothing changes (so callers skip the store write).
* - add/removeLibraryToManifest back the install-declares flow (agent
* install tool, Wokwi InstallLibrariesModal).
*/
import { describe, it, expect } from 'vitest';
import {
normalizeLibName,
bareLibName,
mergeSuggestedLibraries,
addLibraryToManifest,
removeLibraryFromManifest,
} from '../utils/libraryManifest';
describe('normalizeLibName', () => {
it('lowercases and strips non-alphanumerics', () => {
expect(normalizeLibName('Adafruit GFX Library')).toBe('adafruitgfxlibrary');
expect(normalizeLibName('Adafruit_GFX_Library')).toBe('adafruitgfxlibrary');
expect(normalizeLibName('ESP Async WebServer')).toBe('espasyncwebserver');
});
it('handles empty / falsy input', () => {
expect(normalizeLibName('')).toBe('');
});
});
describe('bareLibName', () => {
it('strips a trailing @version', () => {
expect(bareLibName('DHT sensor library@1.4.6')).toBe('DHT sensor library');
expect(bareLibName('ESP32Servo@3.2.1')).toBe('ESP32Servo');
});
it('leaves version-less specs untouched', () => {
expect(bareLibName('ESP32Servo')).toBe('ESP32Servo');
});
it('does not treat a leading @ as a version separator', () => {
expect(bareLibName('@scoped-thing')).toBe('@scoped-thing');
});
});
describe('mergeSuggestedLibraries', () => {
it('adds single-candidate suggestions to an empty manifest', () => {
const merged = mergeSuggestedLibraries(undefined, {
'ESPAsyncWebServer.h': ['ESP Async WebServer'],
'ESP32Servo.h': ['ESP32Servo'],
});
expect(merged).toEqual(['ESP Async WebServer', 'ESP32Servo']);
});
it('skips ambiguous (multi-candidate) suggestions', () => {
const merged = mergeSuggestedLibraries([], {
'Servo.h': ['Servo', 'ESP32Servo'], // ambiguous — never guess
'AsyncTCP.h': ['AsyncTCP'],
});
expect(merged).toEqual(['AsyncTCP']);
});
it('dedups against the current manifest with normalized comparison', () => {
const merged = mergeSuggestedLibraries(['esp async webserver'], {
'ESPAsyncWebServer.h': ['ESP Async WebServer'],
'AsyncTCP.h': ['AsyncTCP'],
});
expect(merged).toEqual(['esp async webserver', 'AsyncTCP']);
});
it('dedups repeated suggestions across headers', () => {
const merged = mergeSuggestedLibraries([], {
'FooA.h': ['FooLib'],
'FooB.h': ['Foo_Lib'], // same library, different spelling
});
expect(merged).toEqual(['FooLib']);
});
it('returns null when there is nothing new (no store churn)', () => {
expect(mergeSuggestedLibraries(['AsyncTCP'], { 'AsyncTCP.h': ['AsyncTCP'] })).toBeNull();
expect(mergeSuggestedLibraries(['A'], null)).toBeNull();
expect(mergeSuggestedLibraries(['A'], undefined)).toBeNull();
expect(mergeSuggestedLibraries(['A'], {})).toBeNull();
});
it('ignores empty or malformed candidate entries', () => {
const merged = mergeSuggestedLibraries([], {
'A.h': [''],
'B.h': [],
'C.h': ['RealLib'],
});
expect(merged).toEqual(['RealLib']);
});
it('preserves the existing manifest order and appends', () => {
const merged = mergeSuggestedLibraries(['First', 'Second'], { 'X.h': ['Third'] });
expect(merged).toEqual(['First', 'Second', 'Third']);
});
});
describe('addLibraryToManifest', () => {
it('adds a bare name from a versioned spec', () => {
expect(addLibraryToManifest(undefined, 'ESP32Servo@3.2.1')).toEqual(['ESP32Servo']);
});
it('returns null when already declared (normalized)', () => {
expect(addLibraryToManifest(['esp32servo'], 'ESP32Servo@3.2.1')).toBeNull();
expect(addLibraryToManifest(['ESP Async WebServer'], 'esp_async_webserver')).toBeNull();
});
it('appends to an existing manifest', () => {
expect(addLibraryToManifest(['A'], 'B')).toEqual(['A', 'B']);
});
it('rejects empty specs', () => {
expect(addLibraryToManifest(['A'], '')).toBeNull();
expect(addLibraryToManifest(['A'], ' ')).toBeNull();
});
});
describe('removeLibraryFromManifest', () => {
it('removes by normalized name regardless of version suffix', () => {
expect(removeLibraryFromManifest(['ESP32Servo', 'AsyncTCP'], 'esp32servo@3.2.1')).toEqual([
'AsyncTCP',
]);
});
it('returns null when the library is not declared', () => {
expect(removeLibraryFromManifest(['A'], 'B')).toBeNull();
expect(removeLibraryFromManifest([], 'B')).toBeNull();
expect(removeLibraryFromManifest(undefined, 'B')).toBeNull();
});
it('can empty the manifest (explicit [] is a valid manifest)', () => {
expect(removeLibraryFromManifest(['OnlyOne'], 'OnlyOne')).toEqual([]);
});
});