|
| 1 | +import { describe, expect, it } from 'vitest' |
| 2 | +import { buildManifest } from '../manifest.js' |
| 3 | +import { type CommandGroup, registry } from '../registry.js' |
| 4 | + |
| 5 | +describe('buildManifest', () => { |
| 6 | + it('stamps the name and the passed-in version', () => { |
| 7 | + const m = buildManifest('9.9.9') |
| 8 | + expect(m.name).toBe('stash') |
| 9 | + expect(m.version).toBe('9.9.9') |
| 10 | + }) |
| 11 | + |
| 12 | + it('emits every non-hidden command across all groups', () => { |
| 13 | + const m = buildManifest('0.0.0') |
| 14 | + const registryCount = registry |
| 15 | + .flatMap((g) => g.commands) |
| 16 | + .filter((c) => !c.hidden).length |
| 17 | + const manifestCount = m.groups.flatMap((g) => g.commands).length |
| 18 | + expect(manifestCount).toBe(registryCount) |
| 19 | + expect(m.groups.length).toBe(registry.length) |
| 20 | + }) |
| 21 | + |
| 22 | + it('drops commands marked hidden', () => { |
| 23 | + // Drive the hidden filter with a stub so coverage doesn't depend on the |
| 24 | + // live registry happening to contain a hidden command (it currently |
| 25 | + // contains none — a real-registry assertion would be vacuously green). |
| 26 | + const groups: CommandGroup[] = [ |
| 27 | + { |
| 28 | + title: 'T', |
| 29 | + commands: [ |
| 30 | + { name: 'shown', summary: 's' }, |
| 31 | + { name: 'gone', summary: 'g', hidden: true }, |
| 32 | + ], |
| 33 | + }, |
| 34 | + ] |
| 35 | + const names = buildManifest('0.0.0', groups) |
| 36 | + .groups.flatMap((g) => g.commands) |
| 37 | + .map((c) => c.name) |
| 38 | + expect(names).toEqual(['shown']) |
| 39 | + }) |
| 40 | + |
| 41 | + it('carries examples through into the manifest', () => { |
| 42 | + // The one optional-field passthrough not covered by the auth-login (long + |
| 43 | + // flags) or wizard (all-undefined) cases. |
| 44 | + const init = buildManifest('0.0.0') |
| 45 | + .groups.flatMap((g) => g.commands) |
| 46 | + .find((c) => c.name === 'init') |
| 47 | + expect(init?.examples).toContain('init --supabase') |
| 48 | + }) |
| 49 | + |
| 50 | + it('defensively copies flags so a consumer cannot corrupt the registry', () => { |
| 51 | + const dbUrlOf = (m: ReturnType<typeof buildManifest>) => |
| 52 | + m.groups |
| 53 | + .flatMap((g) => g.commands) |
| 54 | + .flatMap((c) => c.flags ?? []) |
| 55 | + .find((f) => f.name === '--database-url') |
| 56 | + |
| 57 | + const first = dbUrlOf(buildManifest('0.0.0')) |
| 58 | + expect(first).toBeDefined() |
| 59 | + // Mutate a manifest flag; the shared registry singleton must be untouched. |
| 60 | + ;(first as { description: string }).description = 'MUTATED' |
| 61 | + expect(dbUrlOf(buildManifest('0.0.0'))?.description).not.toBe('MUTATED') |
| 62 | + }) |
| 63 | + |
| 64 | + it('gives every command a non-empty name and summary', () => { |
| 65 | + for (const group of buildManifest('0.0.0').groups) { |
| 66 | + expect(group.title.length).toBeGreaterThan(0) |
| 67 | + for (const cmd of group.commands) { |
| 68 | + expect(cmd.name.length).toBeGreaterThan(0) |
| 69 | + expect(cmd.summary.length).toBeGreaterThan(0) |
| 70 | + } |
| 71 | + } |
| 72 | + }) |
| 73 | + |
| 74 | + it('includes the worked-example auth login descriptor with its flags', () => { |
| 75 | + const cmds = buildManifest('0.0.0').groups.flatMap((g) => g.commands) |
| 76 | + const authLogin = cmds.find((c) => c.name === 'auth login') |
| 77 | + expect(authLogin).toBeDefined() |
| 78 | + expect(authLogin?.long).toContain('device authorization flow') |
| 79 | + const flagNames = authLogin?.flags?.map((f) => f.name) ?? [] |
| 80 | + expect(flagNames).toContain('--region') |
| 81 | + expect(flagNames).toContain('--json') |
| 82 | + expect(flagNames).toContain('--no-open') |
| 83 | + }) |
| 84 | + |
| 85 | + it('surfaces the shared --database-url flag with its env var', () => { |
| 86 | + const eqlInstall = buildManifest('0.0.0') |
| 87 | + .groups.flatMap((g) => g.commands) |
| 88 | + .find((c) => c.name === 'eql install') |
| 89 | + const dbUrl = eqlInstall?.flags?.find((f) => f.name === '--database-url') |
| 90 | + expect(dbUrl?.env).toBe('DATABASE_URL') |
| 91 | + expect(dbUrl?.value).toBe('<url>') |
| 92 | + }) |
| 93 | + |
| 94 | + it('drops undefined optionals so the JSON round-trips cleanly', () => { |
| 95 | + const m = buildManifest('1.2.3') |
| 96 | + const json = JSON.stringify(m) |
| 97 | + expect(JSON.parse(json)).toEqual(m) |
| 98 | + // A summary-only command must not carry empty long/examples/flags keys. |
| 99 | + const wizard = m.groups |
| 100 | + .flatMap((g) => g.commands) |
| 101 | + .find((c) => c.name === 'wizard') |
| 102 | + expect(wizard).toEqual({ |
| 103 | + name: 'wizard', |
| 104 | + summary: 'AI-guided encryption setup (reads your codebase)', |
| 105 | + }) |
| 106 | + }) |
| 107 | +}) |
0 commit comments