import { describe, expect, it } from 'vitest' import { SEMA_VERBS, isSemaVerb, validateEventName, familyForVerb } from './verbs' describe('SEMA_VERBS', () => { it('groups verbs by family per book canon', () => { // Canonical verbs per «Diseñando lo que ocurre» cap. 22-29 expect(SEMA_VERBS.contact).toContain('press') expect(SEMA_VERBS.commit).toContain('toggle') expect(SEMA_VERBS.commit).toContain('save') expect(SEMA_VERBS.signal).toContain('announce') expect(SEMA_VERBS.signal).toContain('warn') expect(SEMA_VERBS.signal).toContain('inform') // cap. 24 expect(SEMA_VERBS.handle).toContain('drag') expect(SEMA_VERBS.emerge).toContain('present') expect(SEMA_VERBS.emerge).toContain('dismiss') expect(SEMA_VERBS.shift).toContain('enter-mode') expect(SEMA_VERBS.sustain).toContain('progress') }) it('exposes the delegate family (book cap. 29)', () => { // Eighth family: who acts. Required for workflows, IA, macros, // rule-based automation. Per book Apéndice C case 3. expect(SEMA_VERBS.delegate).toContain('offer') expect(SEMA_VERBS.delegate).toContain('plan') expect(SEMA_VERBS.delegate).toContain('authorize') expect(SEMA_VERBS.delegate).toContain('act') expect(SEMA_VERBS.delegate).toContain('review') expect(SEMA_VERBS.delegate).toContain('escalate') expect(SEMA_VERBS.delegate).toContain('return') }) it('exposes contextual commit verbs used in the book case studies', () => { // cap. 29 + cap. 30 + apéndice C expect(SEMA_VERBS.commit).toContain('apply') expect(SEMA_VERBS.commit).toContain('partial') expect(SEMA_VERBS.commit).toContain('block') expect(SEMA_VERBS.commit).toContain('upload') }) it('has no internal duplicates per family', () => { for (const verbs of Object.values(SEMA_VERBS)) { expect(new Set(verbs).size).toBe(verbs.length) } }) it('moved select / toggle / acknowledge to commit (per doctrine)', () => { // These were `contact` / `handle` in pre-doctrina shape. expect(SEMA_VERBS.commit).toContain('select') expect(SEMA_VERBS.commit).toContain('toggle') expect(SEMA_VERBS.commit).toContain('acknowledge') expect(SEMA_VERBS.contact as readonly string[]).not.toContain('select') expect(SEMA_VERBS.contact as readonly string[]).not.toContain('toggle') expect(SEMA_VERBS.handle as readonly string[]).not.toContain('acknowledge') }) }) describe('isSemaVerb', () => { it('accepts canonical verbs', () => { expect(isSemaVerb('present')).toBe(true) expect(isSemaVerb('toggle')).toBe(true) expect(isSemaVerb('save')).toBe(true) }) it('rejects non-canonical strings', () => { expect(isSemaVerb('frob')).toBe(false) expect(isSemaVerb('commit-save')).toBe(false) // composite, not a verb }) it('rejects family names (they are not verbs)', () => { expect(isSemaVerb('commit')).toBe(false) expect(isSemaVerb('emerge')).toBe(false) expect(isSemaVerb('signal')).toBe(false) expect(isSemaVerb('delegate')).toBe(false) }) it('rejects non-strings', () => { expect(isSemaVerb(undefined)).toBe(false) expect(isSemaVerb(42)).toBe(false) expect(isSemaVerb(null)).toBe(false) }) }) describe('familyForVerb', () => { it('resolves the family of a canonical verb', () => { expect(familyForVerb('press')).toBe('contact') expect(familyForVerb('toggle')).toBe('commit') expect(familyForVerb('dismiss')).toBe('emerge') expect(familyForVerb('warn')).toBe('signal') expect(familyForVerb('inform')).toBe('signal') expect(familyForVerb('drag')).toBe('handle') expect(familyForVerb('enter-mode')).toBe('shift') expect(familyForVerb('progress')).toBe('sustain') expect(familyForVerb('authorize')).toBe('delegate') expect(familyForVerb('escalate')).toBe('delegate') }) it('returns undefined for non-canonical strings', () => { expect(familyForVerb('frob')).toBeUndefined() expect(familyForVerb('commit')).toBeUndefined() // family name, not a verb }) }) describe('validateEventName', () => { it('matches a bare canonical verb', () => { expect(validateEventName('dismiss')).toEqual({ name: 'dismiss', head: 'dismiss', matchesCanonical: true, variant: undefined, family: 'emerge', verb: 'dismiss' }) }) it('extracts head + variant from verb-variant composites', () => { expect(validateEventName('dismiss-outside')).toEqual({ name: 'dismiss-outside', head: 'dismiss', matchesCanonical: true, variant: 'outside', family: 'emerge', verb: 'dismiss' }) expect(validateEventName('close-cancel')).toEqual({ name: 'close-cancel', head: 'close', matchesCanonical: true, variant: 'cancel', family: 'emerge', verb: 'close' }) }) it('parses family-verb naming (doctrinal shape)', () => { expect(validateEventName('commit-toggle')).toEqual({ name: 'commit-toggle', head: 'commit', matchesCanonical: true, variant: 'toggle', family: 'commit', verb: 'toggle' }) expect(validateEventName('commit-save')).toEqual({ name: 'commit-save', head: 'commit', matchesCanonical: true, variant: 'save', family: 'commit', verb: 'save' }) }) it('flags drift when neither head nor family.verb is canonical', () => { expect(validateEventName('frob-glob')).toEqual({ name: 'frob-glob', head: 'frob', matchesCanonical: false, variant: 'glob', family: undefined, verb: undefined }) }) it('flags drift when family is canonical but the verb is not', () => { expect(validateEventName('commit-frob').matchesCanonical).toBe(false) expect(validateEventName('commit-frob').family).toBe('commit') }) it('recognises multi-word canonical verbs (enter-mode, exit-mode)', () => { // Family-verb shape with multi-word verb. expect(validateEventName('shift-enter-mode')).toEqual({ name: 'shift-enter-mode', head: 'shift', matchesCanonical: true, variant: 'enter-mode', family: 'shift', verb: 'enter-mode' }) expect(validateEventName('shift-exit-mode').matchesCanonical).toBe(true) // With trailing variant. expect(validateEventName('shift-enter-mode-edit').matchesCanonical).toBe(true) expect(validateEventName('shift-enter-mode-edit').verb).toBe('enter-mode') // Bare verb shape — `enter-mode` alone is canonical. expect(validateEventName('enter-mode').matchesCanonical).toBe(true) expect(validateEventName('enter-mode').verb).toBe('enter-mode') expect(validateEventName('enter-mode').family).toBe('shift') }) it('reports the existing dialog event names against the catalog', () => { const dialogEvents = [ 'open', 'close-save', 'close-cancel', 'close-dismiss', 'close-dismiss-outside', 'close-after-fail' ] for (const name of dialogEvents) { const v = validateEventName(name) expect(v.matchesCanonical).toBe(true) } }) it('reports the existing toast event names against the catalog', () => { for (const name of ['present', 'announce', 'dismiss']) { expect(validateEventName(name).matchesCanonical).toBe(true) } }) })