moment-fence.test.ts3.7 KBView on GitHub
import { parseMoment } from '@/modules/documents/coaching/moment-fence';

const BODY = [
  'deal: Works Progress Architecture',
  'meeting: Pirros | Works Progress',
  'criterion: D2',
  'at: 04:10',
  'verdict: bad',
  'coaching: Stay on the PyRevit claim.',
  '',
  '## context',
  '**Michael** (03:50) We have a spreadsheet for each product type.',
  '',
  '## moment',
  '**Michael** (04:10) I am fairly confident we may not be moving forward.',
  '**Keenan** (05:02) So it sounds like there is a need for something.',
  '',
  '## reaction',
  '**Michael** (05:40) Well, we have a number of resources.',
].join('\n');

describe('moment fence', () => {
  it('parses the header, all three sections and speaker/timestamp', () => {
    const m = parseMoment(BODY)!;
    expect(m.deal).toBe('Works Progress Architecture');
    expect(m.criterion).toBe('D2');
    expect(m.verdict).toBe('bad');
    expect(m.coaching).toBe('Stay on the PyRevit claim.');
    expect(m.context).toHaveLength(1);
    expect(m.moment).toHaveLength(2);
    expect(m.reaction).toHaveLength(1);
    expect(m.moment[0].speaker).toBe('Michael');
    expect(m.moment[0].at).toBe('04:10');
    expect(m.moment[0].text).toContain('may not be moving forward');
  });

  it('treats context and reaction as optional', () => {
    const minimal = ['deal: A', 'meeting: B', 'verdict: good', 'coaching: Do it again.', '', '## moment', '**X** (1:00) hi'].join('\n');
    const m = parseMoment(minimal)!;
    expect(m.context).toEqual([]);
    expect(m.reaction).toEqual([]);
    expect(m.moment).toHaveLength(1);
  });

  it('returns null when a required key or the moment section is missing', () => {
    expect(parseMoment('deal: A\nmeeting: B\nverdict: good\n\n## moment\n')).toBeNull();
    expect(parseMoment('deal: A\nmeeting: B\ncoaching: x\n\n## moment\n**X** hi')).toBeNull();
    expect(parseMoment('nonsense')).toBeNull();
  });

  it('rejects an unknown verdict rather than rendering an untoned card', () => {
    expect(parseMoment('deal: A\nmeeting: B\nverdict: spicy\ncoaching: x\n\n## moment\n**X** hi')).toBeNull();
  });
});

describe('moment header fields', () => {
  const FULL = [
    'deal: Cushing Terrell',
    'conversation: 8999bd96-498d-4430-abba-ff25e36ea592',
    'meeting: Pirros | Cushing Terrell',
    'date: 2026-08-21',
    'criterion: D2 Genuinely curious',
    'at: 05:09',
    'recording: https://fathom.video/share/BKMeBJCpLyTqR',
    'verdict: good',
    'coaching: The feelings question got him to name the real risk.',
    '',
    '## moment',
    '**Xander** (05:09) What are your feelings about that?',
  ].join('\n');

  it('parses conversation, date and the Fathom recording link', () => {
    const m = parseMoment(FULL)!;
    expect(m.conversationId).toBe('8999bd96-498d-4430-abba-ff25e36ea592');
    expect(m.date).toBe('2026-08-21');
    expect(m.recording).toContain('fathom.video/share/');
  });

  it('splits the criterion into its id and its readable name', () => {
    const m = parseMoment(FULL)!;
    expect(m.criterion).toBe('D2');
    expect(m.criterionName).toBe('Genuinely curious');
  });

  it('keeps a name-only criterion as the name, with no id', () => {
    const m = parseMoment(FULL.replace('criterion: D2 Genuinely curious', 'criterion: End of call'))!;
    expect(m.criterion).toBeUndefined();
    expect(m.criterionName).toBe('End of call');
  });

  it('still parses when the optional header fields are absent', () => {
    const minimal = ['deal: A', 'meeting: B', 'verdict: bad', 'coaching: x', '', '## moment', '**X** (1:00) hi'].join('\n');
    const m = parseMoment(minimal)!;
    expect(m.conversationId).toBeUndefined();
    expect(m.recording).toBeUndefined();
    expect(m.moment).toHaveLength(1);
  });
});