/** * Error and discovery edge cases for cubes/loader. * * Runs against a real temp directory because loadCubes() dynamically imports * manifest files — there is no seam worth faking here. */ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { findCubeDirectories, getCube, loadCubes } from '../src/cubes/loader.js'; describe('loader edge cases', () => { let originalCwd: string; let originalHome: string | undefined; let tmpDir: string; let emptyHome: string; const cube = (dir: string, manifest: string, deployName = 'deploy.py') => { fs.mkdirSync(path.join(tmpDir, dir), { recursive: true }); fs.writeFileSync(path.join(tmpDir, dir, 'manifest.mjs'), manifest); fs.writeFileSync(path.join(tmpDir, dir, deployName), '# deploy'); }; beforeEach(() => { originalCwd = process.cwd(); originalHome = process.env.HOME; tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'nopy-loader-'))); emptyHome = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'nopy-loader-home-'))); process.env.HOME = emptyHome; process.chdir(tmpDir); fs.writeFileSync(path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: ['./'] })); }); afterEach(() => { process.chdir(originalCwd); if (originalHome === undefined) { delete process.env.HOME; } else { process.env.HOME = originalHome; } fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(emptyHome, { recursive: true, force: true }); }); describe('findCubeDirectories', () => { it('includes directories from cubeDirs', () => { expect(findCubeDirectories()).toContain(tmpDir); }); it('includes directories marked with a .npcubes file', () => { fs.writeFileSync(path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: [] })); fs.writeFileSync(path.join(tmpDir, '.npcubes'), ''); const nested = path.join(tmpDir, 'a', 'b'); fs.mkdirSync(nested, { recursive: true }); process.chdir(nested); expect(findCubeDirectories()).toContain(tmpDir); }); it('does not treat a .npcubes directory as a marker', () => { fs.writeFileSync(path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: [] })); fs.mkdirSync(path.join(tmpDir, '.npcubes')); expect(findCubeDirectories()).not.toContain(tmpDir); }); it('de-duplicates a directory listed twice', () => { fs.writeFileSync( path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: ['./', tmpDir] }) ); fs.writeFileSync(path.join(tmpDir, '.npcubes'), ''); expect(findCubeDirectories().filter((d) => d === tmpDir)).toHaveLength(1); }); }); describe('loadCubes', () => { it('derives the id from a [bracket] name prefix', async () => { cube('bracketed', 'export default { name: "[apt:base] Apt Base" }'); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes['apt:base'].name).toBe('[apt:base] Apt Base'); }); it('falls back to the directory name when no id is derivable', async () => { cube('fallback-id', 'export default { name: "No Id Here" }'); const { cubes } = await loadCubes(); expect(cubes['fallback-id']).toBeDefined(); }); it('defaults the schema when the manifest omits one', async () => { cube('no-schema', 'export default { id: "no-schema", name: "No Schema" }'); const { cubes } = await loadCubes(); expect(cubes['no-schema'].getDefaults()).toEqual({}); }); it('reports a secrets entry that names no schema key', async () => { // A typo here protects nothing and is invisible at runtime — the value // would simply be persisted. cube( 'typo', `import { z } from 'zod'; export default { id: 'typo', name: 'Typo', secrets: ['PASSWROD'], schema: z.object({ PASSWORD: z.string().default('x') }) };` ); const { errors } = await loadCubes(); expect(errors[0]).toMatch(/'secrets' names PASSWROD, which is not in the schema/); }); it('accepts a secrets entry that matches a schema key', async () => { cube( 'ok', `import { z } from 'zod'; export default { id: 'ok', name: 'Ok', secrets: ['PASSWORD'], schema: z.object({ PASSWORD: z.string().default('x') }) };` ); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes.ok.isSecret('PASSWORD')).toBe(true); expect(cubes.ok.isSecret('OTHER')).toBe(false); }); it('reports a manifest whose default export is not an object', async () => { cube('bad-export', 'export default "just a string"'); const { cubes, errors } = await loadCubes(); expect(cubes['bad-export']).toBeUndefined(); expect(errors[0]).toMatch(/Invalid manifest export/); }); it('reports a manifest with no default export', async () => { cube('no-export', 'export const nothing = 1;'); const { errors } = await loadCubes(); expect(errors[0]).toMatch(/Invalid manifest export/); }); it('reports a manifest missing a name', async () => { cube('no-name', 'export default { id: "no-name" }'); const { errors } = await loadCubes(); expect(errors[0]).toMatch(/missing 'name'/); }); it('reports a manifest that fails to import', async () => { cube('broken', 'this is not valid javascript !!!'); const { errors } = await loadCubes(); expect(errors[0]).toMatch(/Failed to load manifest/); }); it('reports duplicate cube ids, naming every directory that claims one', async () => { cube('first', 'export default { id: "dup", name: "First" }'); cube('second', 'export default { id: "dup", name: "Second" }'); const { cubes, errors } = await loadCubes(); expect(Object.keys(cubes)).toEqual(['dup']); expect(errors).toHaveLength(1); expect(errors[0]).toMatch(/Duplicate cube id 'dup' from 2 sources/); expect(errors[0]).toContain(path.join(tmpDir, 'first')); expect(errors[0]).toContain(path.join(tmpDir, 'second')); }); it('keeps scanning below a duplicate instead of dropping the subtree', async () => { cube('first', 'export default { id: "dup", name: "First" }'); cube('second', 'export default { id: "dup", name: "Second" }'); cube('second/inner', 'export default { id: "buried", name: "Buried" }'); const { cubes, errors } = await loadCubes(); expect(cubes.buried).toBeDefined(); expect(errors).toHaveLength(1); }); it('reports the same duplicate whichever root is scanned first', async () => { cube('a/one', 'export default { id: "dup", name: "One" }'); cube('b/two', 'export default { id: "dup", name: "Two" }'); const roots = [path.join(tmpDir, 'a'), path.join(tmpDir, 'b')]; fs.writeFileSync(path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: roots })); const forwards = await loadCubes(); fs.writeFileSync( path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: [...roots].reverse() }) ); const backwards = await loadCubes(); expect(forwards.errors[0]).toContain(path.join(tmpDir, 'a', 'one')); expect(forwards.errors[0]).toContain(path.join(tmpDir, 'b', 'two')); expect(backwards.errors).toHaveLength(1); expect(new Set(backwards.errors[0].split('\n'))).toEqual( new Set(forwards.errors[0].split('\n')) ); }); it('does not call one directory a duplicate of itself when two roots reach it', async () => { cube('nested/one', 'export default { id: "once", name: "Once" }'); fs.writeFileSync( path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: ['./', './nested'] }) ); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes.once).toBeDefined(); }); it('skips hidden and node_modules directories', async () => { cube('.hidden/inner', 'export default { id: "hidden", name: "Hidden" }'); cube('node_modules/pkg', 'export default { id: "vendored", name: "Vendored" }'); cube('visible', 'export default { id: "visible", name: "Visible" }'); const { cubes } = await loadCubes(); expect(Object.keys(cubes)).toEqual(['visible']); }); it('ignores configured cube directories that do not exist', async () => { fs.writeFileSync( path.join(tmpDir, '.nopyrc.json'), JSON.stringify({ cubeDirs: ['./', './does-not-exist'] }) ); cube('visible', 'export default { id: "visible", name: "Visible" }'); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes.visible).toBeDefined(); }); }); describe('cubePackages', () => { /** Installs a cube package into the temp project's node_modules. */ const installPackage = (name: string, cubeId: string) => { const root = path.join(tmpDir, 'node_modules', name); const cubeDir = path.join(root, 'cubes', cubeId); fs.mkdirSync(cubeDir, { recursive: true }); fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify({ name, nopy: { cubes: ['./cubes'] } }) ); fs.writeFileSync( path.join(cubeDir, 'manifest.mjs'), `export default { id: "${cubeId}", name: "From a package" }` ); fs.writeFileSync(path.join(cubeDir, 'deploy.py'), '# deploy'); return cubeDir; }; const config = (extra: Record) => fs.writeFileSync(path.join(tmpDir, '.nopyrc.json'), JSON.stringify(extra)); it('loads cubes from a package and records where they came from', async () => { const cubeDir = installPackage('@acme/cubes-net', 'net:vpn'); config({ cubeDirs: [], cubePackages: ['@acme/cubes-net'] }); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes['net:vpn'].dir).toBe(cubeDir); expect(cubes['net:vpn'].source).toEqual({ type: 'package', packageName: '@acme/cubes-net', dir: path.join(tmpDir, 'node_modules', '@acme/cubes-net', 'cubes'), }); }); it('marks a cube found under a plain directory as directory-sourced', async () => { cube('local', 'export default { id: "local", name: "Local" }'); const { cubes } = await loadCubes(); expect(cubes.local.source).toEqual({ type: 'dir', dir: tmpDir }); }); it('still skips a node_modules tree nobody asked for', async () => { installPackage('@acme/cubes-net', 'net:vpn'); config({ cubeDirs: ['./'] }); const { cubes, errors } = await loadCubes(); expect(errors).toEqual([]); expect(cubes['net:vpn']).toBeUndefined(); }); it('names the package and the directory when both claim one id', async () => { installPackage('@acme/cubes-net', 'clash'); cube('local', 'export default { id: "clash", name: "Local" }'); config({ cubeDirs: ['./'], cubePackages: ['@acme/cubes-net'] }); const { errors } = await loadCubes(); expect(errors).toHaveLength(1); expect(errors[0]).toMatch(/Duplicate cube id 'clash' from 2 sources/); // Labels are padded to a common width, so match the pair, not the gap. expect(errors[0]).toMatch( new RegExp(`^\\s+directory\\s+${path.join(tmpDir, 'local')}$`, 'm') ); expect(errors[0]).toMatch( new RegExp( `^\\s+package @acme/cubes-net\\s+${path.join(tmpDir, 'node_modules/@acme/cubes-net/cubes/clash')}$`, 'm' ) ); }); it('aborts when a named package is not installed', async () => { config({ cubeDirs: [], cubePackages: ['@acme/missing'] }); const { cubes, errors } = await loadCubes(); expect(cubes).toEqual({}); expect(errors[0]).toMatch(/'@acme\/missing' is not installed/); }); }); describe('getCube', () => { it('returns a single cube by id', async () => { cube('one', 'export default { id: "one", name: "One" }'); await expect(getCube('one')).resolves.toMatchObject({ id: 'one' }); }); it('returns undefined for an unknown id', async () => { await expect(getCube('nope')).resolves.toBeUndefined(); }); }); });