/** * Tests for the import-routing rule that disambiguates between * `celilo module import caddy` → registry * `celilo module import ./modules/caddy` → local path * * Pure function over the argument string; no network, no filesystem. */ import { describe, expect, test } from 'bun:test'; import { classifyImportArg, handleModuleImport } from './module-import'; describe('classifyImportArg', () => { test('routes bare kebab names to the registry', () => { expect(classifyImportArg('caddy')).toBe('name'); expect(classifyImportArg('homebridge')).toBe('name'); expect(classifyImportArg('dns-external')).toBe('name'); expect(classifyImportArg('a')).toBe('name'); }); test('routes leading-dot relative paths to the filesystem', () => { expect(classifyImportArg('./modules/caddy')).toBe('path'); expect(classifyImportArg('../caddy')).toBe('path'); expect(classifyImportArg('./caddy')).toBe('path'); }); test('routes leading-slash absolute paths to the filesystem', () => { expect(classifyImportArg('/tmp/caddy')).toBe('path'); expect(classifyImportArg('/abs/path/to/module')).toBe('path'); }); test('routes tilde-expanded paths to the filesystem', () => { expect(classifyImportArg('~')).toBe('path'); expect(classifyImportArg('~/dev/caddy')).toBe('path'); }); test('routes any path containing / to the filesystem', () => { expect(classifyImportArg('modules/caddy')).toBe('path'); expect(classifyImportArg('a/b/c')).toBe('path'); }); test('routes .netapp filenames to the filesystem (registry never serves them by name)', () => { expect(classifyImportArg('caddy.netapp')).toBe('path'); expect(classifyImportArg('homebridge-1.0.0.netapp')).toBe('path'); }); test('does not confuse versioned names with paths (no slash, no .netapp)', () => { // Future: registry may accept `name@version` syntax. Today this routes // to "name" — KEBAB_NAME validation in handleModuleImport will reject // until the syntax is implemented. expect(classifyImportArg('caddy@1.0.0')).toBe('name'); }); }); describe('legacy subcommand migration hints', () => { test('catches `module import file ` and suggests the new form', async () => { const result = await handleModuleImport(['file', 'celilo-registry'], {}); if (result.success) throw new Error('expected failure'); expect(result.error).toContain("'file' is no longer a subcommand"); // 'file' was the LOCAL form; a bare name now goes to the registry, // so the hint must lead the operator to a path-shaped form. expect(result.error).toContain('./celilo-registry'); expect(result.error).toContain('/abs/path/to/celilo-registry'); }); test('catches `module import public-registry ` and suggests the new form', async () => { const result = await handleModuleImport(['public-registry', 'caddy'], {}); if (result.success) throw new Error('expected failure'); expect(result.error).toContain("'public-registry' is no longer a subcommand"); expect(result.error).toContain('celilo module import caddy'); }); test('does not catch a real registry name that happens to share no characters with legacy commands', async () => { // Sanity: caddy → registry lookup. We can't fully exercise this without // network mocks, but we can confirm it doesn't fall through the legacy // branch (the error message would mention "no longer a subcommand"). const result = await handleModuleImport(['caddy'], { registry: 'http://0.0.0.0:1' }); if (result.success) throw new Error('expected failure'); expect(result.error).not.toContain('no longer a subcommand'); }); });