File size: 3,453 Bytes
ec8acdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * #5622 — the analyst panel's denial copy, as an executable table.
 *
 * `src/components/ChatAnalystPanel.ts` imports DOMPurify and `marked` at module
 * scope, so `describeDenial` was unreachable from `tsx --test` and its new
 * billing-verification branch shipped with no coverage. The decision now lives
 * in a zero-runtime-import leaf; this pins it.
 *
 * The branch that matters: a retryable 503 must NOT fall through to
 * `classifyDenialResponse`, which owns only 401/403 and returns null for a 5xx —
 * that fallthrough renders `Error 503`, the least actionable string we have, for
 * the one state where the user's subscription is fine and waiting works.
 */

import { strict as assert } from 'node:assert';
import test from 'node:test';

import {
  analystDenialMessage,
  isBillingVerificationDenial,
  PRO_VERIFICATION_RETRY_MESSAGE,
} from '../src/services/analyst-denial.ts';

test('a 503 carrying X-Billing-Verification is the retryable branch', () => {
  assert.equal(
    isBillingVerificationDenial(503, 'entitlement_verification_unavailable'),
    true,
  );
  assert.equal(isBillingVerificationDenial(503, 'renewal_verification_pending'), true);
  assert.equal(isBillingVerificationDenial(503, 'renewal_verification_failed'), true);
});

test('a 503 WITHOUT the header is not the billing branch', () => {
  // api/notification-channels.ts and friends answer a bare 503 for missing env
  // and for relay failures. Those are not retryable billing states and must not
  // borrow the "your subscription is fine" copy.
  assert.equal(isBillingVerificationDenial(503, null), false);
  assert.equal(isBillingVerificationDenial(503, ''), false);
});

test('the header alone does not make a non-503 retryable', () => {
  // A terminal 403 lapse also carries X-Billing-Verification (subscription_lapsed).
  // Keying on the header alone would turn a resubscribe into a retry loop.
  assert.equal(isBillingVerificationDenial(403, 'subscription_lapsed'), false);
  assert.equal(isBillingVerificationDenial(401, 'entitlement_verification_unavailable'), false);
  assert.equal(isBillingVerificationDenial(200, 'entitlement_verification_unavailable'), false);
});

test('verdict-to-copy mapping covers every PremiumDenialVerdict member', () => {
  assert.equal(analystDenialMessage(401, 'sign_in_required'), 'Sign in to use the analyst.');
  assert.equal(analystDenialMessage(403, 'upgrade_required'), 'Pro subscription required.');
  assert.equal(
    analystDenialMessage(403, 'access_denied'),
    'Analyst temporarily unavailable — try again in a moment.',
  );
});

test('entitlement_desync shares copy with the billing-verification 503', () => {
  // Same situation from the user's side: we cannot confirm your access, and
  // retrying helps. If these ever diverge it should be a deliberate edit.
  assert.equal(analystDenialMessage(403, 'entitlement_desync'), PRO_VERIFICATION_RETRY_MESSAGE);
});

test('an unclassified denial falls back to the bare status', () => {
  assert.equal(analystDenialMessage(503, null), 'Error 503');
  assert.equal(analystDenialMessage(500, null), 'Error 500');
});

test('the retryable copy never tells a paying customer to subscribe', () => {
  // The #5600 regression in one assertion: the transient state must not render
  // the upsell string.
  assert.notEqual(PRO_VERIFICATION_RETRY_MESSAGE, 'Pro subscription required.');
  assert.ok(!/subscri/i.test(PRO_VERIFICATION_RETRY_MESSAGE));
});