velxio/frontend/src/__tests__/micropython-libs-service.te...

81 lines
3.0 KiB
TypeScript
Raw Normal View History

feat(micropython): install libraries from the micropython-lib index (#214) A MicroPython library in Velxio IS a .py file in the project, copied onto the board's filesystem before main.py runs. That mechanism worked, but getting the file meant hunting GitHub by hand — the gap issue #214 is about. Now the Library Manager fills it, in both halves: Backend: /api/micropython-libs proxies the official micropython-lib package index (the same one `mip` uses on real hardware). /search filters the index; /fetch resolves a package plus its declared deps into .py files. The proxy is what makes this safe to expose: only the official index is reachable (the name must exist in index.json, files come from its content-addressed store), source only (.py, never .mpy bytecode), path- safe, and capped at 24 files / 256 KB per install. In-process TTL cache over the index. Frontend: the same Library Manager modal, mode-aware. On a MicroPython board the search hits the index and "Add to project" writes the package's files into the board's workspace (upsert — re-adding is the upgrade path), visible in the explorer and travelling with the project. Before typing it shows featured picks (ssd1306, umqtt.*, urequests) and the modules already frozen into the firmware (neopixel, dht, onewire, ds18x20, machine, network) labeled "built into the firmware", so searching for one answers "just import it" instead of an empty list. The file-materialization prelude now creates package directories: umqtt ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the prelude died with ENOENT on any packaged module. Also ships a gallery example ("ESP32 + MicroPython: use an external library") that documents the file-is-the-library mechanism with the ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
/**
* micropythonLibs service (issue #214) — the workspace-write half.
*
* The backend resolves a micropython-lib package into .py files; this side
* upserts them into the board's file group. Pinned here: upsert semantics
* (re-adding = upgrade, not duplicate), package-dir paths, and the
* in-project detection the modal's pills rely on.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useEditorStore } from '../store/useEditorStore';
import {
packageFilesInGroup,
writeFilesIntoGroup,
BUILTIN_MPY_MODULES,
FEATURED_MPY_PACKAGES,
} from '../services/micropythonLibs';
const GROUP = 'group-mpy-test';
describe('writeFilesIntoGroup', () => {
beforeEach(() => {
const ed = useEditorStore.getState();
ed.deleteFileGroup(GROUP);
ed.createFileGroup(GROUP, [{ name: 'main.py', content: 'print(1)\n' }]);
ed.setActiveGroup(GROUP);
});
it('adds the package files next to main.py', () => {
const written = writeFilesIntoGroup(GROUP, [
{ path: 'ssd1306.py', content: '# driver\n' },
]);
expect(written).toEqual(['ssd1306.py']);
const names = useEditorStore.getState().getGroupFiles(GROUP).map((f) => f.name);
expect(names).toContain('main.py');
expect(names).toContain('ssd1306.py');
});
it('re-adding overwrites instead of duplicating (that is the upgrade path)', () => {
writeFilesIntoGroup(GROUP, [{ path: 'ssd1306.py', content: 'v1\n' }]);
writeFilesIntoGroup(GROUP, [{ path: 'ssd1306.py', content: 'v2\n' }]);
const files = useEditorStore.getState().getGroupFiles(GROUP);
const matches = files.filter((f) => f.name === 'ssd1306.py');
expect(matches).toHaveLength(1);
expect(matches[0].content).toBe('v2\n');
});
it('handles package-dir paths the way micropython-lib ships them', () => {
writeFilesIntoGroup(GROUP, [{ path: 'umqtt/simple.py', content: '# mqtt\n' }]);
const names = useEditorStore.getState().getGroupFiles(GROUP).map((f) => f.name);
expect(names).toContain('umqtt/simple.py');
});
});
describe('packageFilesInGroup', () => {
beforeEach(() => {
const ed = useEditorStore.getState();
ed.deleteFileGroup(GROUP);
ed.createFileGroup(GROUP, [{ name: 'main.py', content: '' }]);
ed.setActiveGroup(GROUP);
});
it('detects a plain module and a dotted package', () => {
expect(packageFilesInGroup(GROUP, 'ssd1306')).toBe(false);
writeFilesIntoGroup(GROUP, [{ path: 'ssd1306.py', content: 'x' }]);
expect(packageFilesInGroup(GROUP, 'ssd1306')).toBe(true);
expect(packageFilesInGroup(GROUP, 'umqtt.simple')).toBe(false);
writeFilesIntoGroup(GROUP, [{ path: 'umqtt/simple.py', content: 'x' }]);
expect(packageFilesInGroup(GROUP, 'umqtt.simple')).toBe(true);
});
});
describe('curated lists', () => {
it('featured picks never claim firmware builtins (one truth per module)', () => {
const builtins = new Set(BUILTIN_MPY_MODULES.map((b) => b.name));
for (const f of FEATURED_MPY_PACKAGES) {
expect(builtins.has(f.name)).toBe(false);
}
});
});