describeProposedMutation.test.ts4.9 KBView on GitHub
/**
 * What an approval card says about the thing it is about to change.
 *
 * The regression these pin: a delete card that named only its ACTION. "Delete document"
 * over a bare `documentId` asked the user to approve destroying something the card never
 * identified — and a chat that proposed two hundred of them was two hundred identical
 * cards. Every card type carries its subject somewhere in its payload; these tests fix
 * where each one reads it from.
 */
import { describeProposedMutation } from '@/modules/cedar-os/src/store/messages/renderers/describeProposedMutation';

describe('describeProposedMutation — proposedDocumentDelete', () => {
  it('names the document and shows where it lives', () => {
    const d = describeProposedMutation({
      type: 'proposedDocumentDelete',
      documentId: 'c1d42e1c-5b1a-4abf-9b4e-bd64af5ddc49',
      documentTitle: 'champion-lacks-authority-to-approve-pilot',
      documentPath: 'organisation/wiki-legacy/objections/champion-lacks-authority-to-approve-pilot',
    });

    expect(d.label).toBe('Delete document');
    expect(d.subject).toBe('champion-lacks-authority-to-approve-pilot');
    expect(d.detail).toBe(
      'organisation/wiki-legacy/objections/champion-lacks-authority-to-approve-pilot',
    );
    expect(d.tone).toBe('destructive');
    expect(d.actionLabel).toBe('Delete');
  });

  it('falls back to the path leaf when the card carries no title', () => {
    const d = describeProposedMutation({
      type: 'proposedDocumentDelete',
      documentId: 'doc-1',
      documentPath: 'user/notes/2026-05-04-daily-checklist',
    });

    expect(d.subject).toBe('2026-05-04-daily-checklist');
  });

  it('falls back to the id — never to nothing — on a card persisted before names were sent', () => {
    const d = describeProposedMutation({ type: 'proposedDocumentDelete', documentId: 'doc-1' });

    expect(d.subject).toBe('doc-1');
  });
});

describe('describeProposedMutation — the other card types', () => {
  it('reads a share card from its path, and flips the verbs on a revoke', () => {
    const share = describeProposedMutation({
      type: 'proposedDocumentShare',
      path: 'user/files/pricing-one-pager',
    });
    expect(share.subject).toBe('pricing-one-pager');
    expect(share.actionLabel).toBe('Share');

    const revoke = describeProposedMutation({
      type: 'proposedDocumentShare',
      path: 'user/files/pricing-one-pager',
      revoke: true,
    });
    expect(revoke.label).toBe('Revoke document sharing');
    expect(revoke.actionLabel).toBe('Revoke');
  });

  it('shows a field delete as destructive, and says what goes with the field', () => {
    const d = describeProposedMutation({
      type: 'proposedFieldDelete',
      fieldKey=[redacted],
      valueCount: 42,
    });

    expect(d.subject).toBe('renewal_risk');
    expect(d.detail).toBe('Also deletes 42 stored values');
    expect(d.tone).toBe('destructive');
  });

  it('singularizes a lone stored value', () => {
    const d = describeProposedMutation({
      type: 'proposedFieldDelete',
      fieldKey=[redacted],
      valueCount: 1,
    });

    expect(d.detail).toBe('Also deletes 1 stored value');
  });

  it('reads an AOP reassignment as the move it is', () => {
    const d = describeProposedMutation({
      type: 'proposedAopReassignment',
      previousAop: 'new_business',
      newAop: 'renewals_expansion',
      reasoning: 'The deal closed and is now an expansion motion.',
    });

    expect(d.subject).toBe('New business → Renewals expansion');
    expect(d.detail).toBe('The deal closed and is now an expansion motion.');
  });

  it('treats a wiki write as non-destructive and a retract as destructive', () => {
    expect(
      describeProposedMutation({
        type: 'proposedWikiWrite',
        path: 'objections/security-review',
        title: 'Security review stalls',
      }),
    ).toMatchObject({ subject: 'Security review stalls', tone: 'neutral' });

    expect(
      describeProposedMutation({
        type: 'proposedWikiRetract',
        path: 'objections/security-review',
        reason: 'Superseded by the SOC2 page',
      }),
    ).toMatchObject({ subject: 'security-review', tone: 'destructive' });
  });

  it('puts the notification text itself on the card', () => {
    const d = describeProposedMutation({
      type: 'proposedNotification',
      text: 'Two deals went quiet this week.',
      agentName: 'Pipeline review',
    });

    expect(d.label).toBe('Notification from Pipeline review');
    expect(d.subject).toBe('Two deals went quiet this week.');
    expect(d.actionLabel).toBe('Send');
  });

  it('falls back to a generic confirm for a card type it has never seen', () => {
    const d = describeProposedMutation({
      type: 'proposedSomethingNew',
      summary: 'Do the new thing',
    });

    expect(d.label).toBe('Confirm change');
    expect(d.subject).toBe('Do the new thing');
    expect(d.tone).toBe('neutral');
  });
});