evaldoc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.env.staging.example +19 -0
  2. package/.firebaserc +6 -0
  3. package/.github/workflows/auto-merge-to-main.yml +28 -0
  4. package/.github/workflows/ci.yml +27 -0
  5. package/.github/workflows/npxhub-publish.yml +170 -0
  6. package/.prettierignore +4 -0
  7. package/.prettierrc.json +7 -0
  8. package/README.md +142 -0
  9. package/SRS.md +448 -0
  10. package/apphosting.staging.yaml +48 -0
  11. package/apphosting.yaml +50 -0
  12. package/bin/evaldoc.cjs +105 -0
  13. package/cors.json +8 -0
  14. package/docs/ARCHITECTURE.md +769 -0
  15. package/docs/ARCHITECTURE_SUMMARY.md +323 -0
  16. package/docs/DEPLOYMENT.md +86 -0
  17. package/docs/DEPLOYMENT_STEPS.md +373 -0
  18. package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
  19. package/docs/DEPLOY_STORAGE_RULES.md +82 -0
  20. package/docs/DOCTOR_TUTORIAL.md +467 -0
  21. package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
  22. package/docs/FIXES_APPLIED.md +128 -0
  23. package/docs/IMPLEMENTATION.md +498 -0
  24. package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
  25. package/docs/LLM-SETUP.md +216 -0
  26. package/docs/MANUAL_TEST_GUIDE.md +327 -0
  27. package/docs/PATIENT_GUIDE.md +451 -0
  28. package/docs/PLATFORM_OVERVIEW.md +194 -0
  29. package/docs/QUICKSTART.md +252 -0
  30. package/docs/QUICK_REFERENCE.md +222 -0
  31. package/docs/READY_TO_TEST.md +383 -0
  32. package/docs/RUN.md +237 -0
  33. package/docs/START_TESTING.md +130 -0
  34. package/docs/STRIPE_CONNECT_SETUP.md +454 -0
  35. package/docs/SYSTEM_READY.md +345 -0
  36. package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
  37. package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
  38. package/docs/firebasefiles.md +21 -0
  39. package/docs/homepage.md +1208 -0
  40. package/docs/payments.md +159 -0
  41. package/eslint.config.js +76 -0
  42. package/firebase.json +27 -0
  43. package/firestore.indexes.json +88 -0
  44. package/firestore.rules +125 -0
  45. package/functions/package-lock.json +2884 -0
  46. package/functions/package.json +20 -0
  47. package/functions/src/analytics.js +101 -0
  48. package/functions/src/audit.js +28 -0
  49. package/functions/src/auth.js +19 -0
  50. package/functions/src/drafts.js +128 -0
  51. package/functions/src/feedback.js +164 -0
  52. package/functions/src/firebase-init.js +29 -0
  53. package/functions/src/i18n.js +134 -0
  54. package/functions/src/limits.js +27 -0
  55. package/functions/src/llm.js +271 -0
  56. package/functions/src/ner.js +162 -0
  57. package/functions/src/notifications.js +273 -0
  58. package/functions/src/orgs.js +127 -0
  59. package/functions/src/patient-api.js +545 -0
  60. package/functions/src/payment.js +830 -0
  61. package/functions/src/safety.js +188 -0
  62. package/functions/src/stripe-config.js +33 -0
  63. package/functions/src/topics.js +94 -0
  64. package/functions/src/triage.js +191 -0
  65. package/functions/src/utils.js +28 -0
  66. package/functions/src/validate.js +177 -0
  67. package/functions/test-firebase.js +36 -0
  68. package/jest.config.js +18 -0
  69. package/package.json +52 -0
  70. package/public/404.html +93 -0
  71. package/public/app/analytics.html +336 -0
  72. package/public/app/analytics.js +177 -0
  73. package/public/app/app.css +1400 -0
  74. package/public/app/app.js +1754 -0
  75. package/public/app/config.js +14 -0
  76. package/public/app/inbox-threads.js +843 -0
  77. package/public/app/inbox.html +598 -0
  78. package/public/app/inbox.js +397 -0
  79. package/public/app/index.html +612 -0
  80. package/public/assets/logo.jpg +0 -0
  81. package/public/chat.html +137 -0
  82. package/public/css/chat.css +870 -0
  83. package/public/css/landing-enhance.css +298 -0
  84. package/public/css/styles.css +1198 -0
  85. package/public/favicon.svg +5 -0
  86. package/public/find-doctor.html +220 -0
  87. package/public/index.html +399 -0
  88. package/public/js/chat-threads.js +513 -0
  89. package/public/js/chat.js +832 -0
  90. package/public/js/search.js +41 -0
  91. package/public/privacy.html +101 -0
  92. package/public/robots.txt +6 -0
  93. package/public/site.webmanifest +12 -0
  94. package/public/terms.html +110 -0
  95. package/scripts/create-test-provider.js +55 -0
  96. package/scripts/deploy-staging.sh +51 -0
  97. package/scripts/fix-provider.js +39 -0
  98. package/scripts/quick-test.sh +37 -0
  99. package/scripts/test-chat.sh +54 -0
  100. package/scripts/test-multi-turn.js +137 -0
  101. package/scripts/test-ner-simple.js +28 -0
  102. package/scripts/test-ner.js +60 -0
  103. package/scripts/test-stripe-connect.js +304 -0
  104. package/scripts/test-webhook-secret.sh +24 -0
  105. package/server.js +3864 -0
  106. package/storage.rules +22 -0
  107. package/test-api.json +1 -0
  108. package/tests/emulator/README.md +38 -0
  109. package/tests/emulator/counters.emulator.test.js +81 -0
  110. package/tests/integration/api-account-selfserve.test.js +312 -0
  111. package/tests/integration/api-admin-digest.test.js +202 -0
  112. package/tests/integration/api-chat-satisfaction.test.js +259 -0
  113. package/tests/integration/api-doctor-drafts.test.js +236 -0
  114. package/tests/integration/api-doctor-inbox.test.js +401 -0
  115. package/tests/integration/api-org.test.js +328 -0
  116. package/tests/integration/api-payment.test.js +664 -0
  117. package/tests/integration/api-provider-settings.test.js +475 -0
  118. package/tests/integration/api-qr.test.js +121 -0
  119. package/tests/integration/api-security.test.js +241 -0
  120. package/tests/integration/api-stripe-search.test.js +303 -0
  121. package/tests/integration/api-webhook-deletion.test.js +231 -0
  122. package/tests/mocks/firebase-admin.js +220 -0
  123. package/tests/unit/analytics.test.js +151 -0
  124. package/tests/unit/audit-bugs.test.js +203 -0
  125. package/tests/unit/chat-ui-logic.test.js +283 -0
  126. package/tests/unit/drafts.test.js +104 -0
  127. package/tests/unit/feedback.test.js +148 -0
  128. package/tests/unit/i18n.test.js +229 -0
  129. package/tests/unit/llm-retry.test.js +122 -0
  130. package/tests/unit/notifications.test.js +155 -0
  131. package/tests/unit/orgs.test.js +328 -0
  132. package/tests/unit/payment.test.js +889 -0
  133. package/tests/unit/safety-edge-cases.test.js +509 -0
  134. package/tests/unit/safety-srs-compliance.test.js +252 -0
  135. package/tests/unit/safety.test.js +291 -0
  136. package/tests/unit/server-helpers.test.js +774 -0
  137. package/tests/unit/topics-limits.test.js +158 -0
  138. package/tests/unit/triage.test.js +152 -0
  139. package/tests/unit/validate.test.js +408 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Deep safety pipeline tests — SRS sections 7.1, 7.2, 7.3 compliance
3
+ *
4
+ * Every blocked/escalation pattern from the SRS is tested individually.
5
+ */
6
+
7
+ jest.mock('firebase-admin', () => require('../mocks/firebase-admin'));
8
+
9
+ const {
10
+ analyzeInput,
11
+ analyzeOutput,
12
+ redactText,
13
+ refusalMessage,
14
+ escalationMessage,
15
+ INPUT_BLOCKED,
16
+ OUTPUT_BLOCKED,
17
+ GLOBAL_REDFLAGS,
18
+ PII_PATTERNS
19
+ } = require('../../functions/src/safety');
20
+
21
+ describe('SRS 7.1 — Always Block (Input)', () => {
22
+ describe('Diagnosis claims', () => {
23
+ it('should block "give me a diagnosis"', async () => {
24
+ const result = await analyzeInput('give me a diagnosis');
25
+ expect(result.blocked).toBe(true);
26
+ });
27
+ });
28
+
29
+ describe('Treatment instructions', () => {
30
+ it('should block "prescribe me something"', async () => {
31
+ const result = await analyzeInput('prescribe me antibiotics');
32
+ expect(result.blocked).toBe(true);
33
+ });
34
+
35
+ it('should block "write me a prescription"', async () => {
36
+ const result = await analyzeInput('write me a prescription for pain');
37
+ expect(result.blocked).toBe(true);
38
+ });
39
+
40
+ it('should block "recommend medication"', async () => {
41
+ const result = await analyzeInput('recommend medication for my cold');
42
+ expect(result.blocked).toBe(true);
43
+ });
44
+
45
+ it('should block "best treatment for"', async () => {
46
+ const result = await analyzeInput('what is the best treatment for flu?');
47
+ expect(result.blocked).toBe(true);
48
+ });
49
+
50
+ it('should block "what should I take for"', async () => {
51
+ const result = await analyzeInput('what should i take for a headache?');
52
+ expect(result.blocked).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('Dosing instructions', () => {
57
+ it('should block "how many tablets"', async () => {
58
+ const result = await analyzeInput('how many tablets should I take?');
59
+ expect(result.blocked).toBe(true);
60
+ });
61
+
62
+ it('should block "how many pills"', async () => {
63
+ const result = await analyzeInput('how many pills do I need?');
64
+ expect(result.blocked).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('Off-label suggestions', () => {
69
+ it('should block "off-label" mentions', async () => {
70
+ const result = await analyzeInput('can I use this off-label?');
71
+ expect(result.blocked).toBe(true);
72
+ });
73
+ });
74
+
75
+ describe('Medical image interpretation', () => {
76
+ it('should block "interpret my x-ray"', async () => {
77
+ const result = await analyzeInput('can you interpret my x-ray results?');
78
+ expect(result.blocked).toBe(true);
79
+ });
80
+
81
+ it('should block "interpret my MRI"', async () => {
82
+ const result = await analyzeInput('interpret my MRI scan please');
83
+ expect(result.blocked).toBe(true);
84
+ });
85
+
86
+ it('should block "image shows"', async () => {
87
+ const result = await analyzeInput('the image shows a dark spot');
88
+ expect(result.blocked).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe('Safe inputs should NOT be blocked', () => {
93
+ it('should allow general health questions', async () => {
94
+ const result = await analyzeInput('What are the symptoms of a cold?');
95
+ expect(result.blocked).toBe(false);
96
+ });
97
+
98
+ it('should allow appointment questions', async () => {
99
+ const result = await analyzeInput('How do I schedule an appointment?');
100
+ expect(result.blocked).toBe(false);
101
+ });
102
+
103
+ it('should allow insurance questions', async () => {
104
+ const result = await analyzeInput('Do you accept Blue Cross insurance?');
105
+ expect(result.blocked).toBe(false);
106
+ });
107
+ });
108
+ });
109
+
110
+ describe('SRS 7.1 — Always Block (Output)', () => {
111
+ it('should block "you have [condition]"', () => {
112
+ const result = analyzeOutput('You have diabetes based on your symptoms');
113
+ expect(result.blocked).toBe(true);
114
+ });
115
+
116
+ it('should NOT block "you have questions"', () => {
117
+ const result = analyzeOutput('If you have questions, please ask');
118
+ expect(result.blocked).toBe(false);
119
+ });
120
+
121
+ it('should NOT block "you have concerns"', () => {
122
+ const result = analyzeOutput('If you have concerns, consult your doctor');
123
+ expect(result.blocked).toBe(false);
124
+ });
125
+
126
+ it('should block "you are suffering from"', () => {
127
+ const result = analyzeOutput('You are suffering from depression');
128
+ expect(result.blocked).toBe(true);
129
+ });
130
+
131
+ it('should block "this is likely"', () => {
132
+ const result = analyzeOutput('this is likely a bacterial infection');
133
+ expect(result.blocked).toBe(true);
134
+ });
135
+
136
+ it('should block dosing: "take 500mg"', () => {
137
+ const result = analyzeOutput('Take 500mg of ibuprofen twice daily');
138
+ expect(result.blocked).toBe(true);
139
+ });
140
+
141
+ it('should block "start taking"', () => {
142
+ const result = analyzeOutput('You should start taking metformin');
143
+ expect(result.blocked).toBe(true);
144
+ });
145
+
146
+ it('should block "stop using"', () => {
147
+ const result = analyzeOutput('Stop using aspirin immediately');
148
+ expect(result.blocked).toBe(true);
149
+ });
150
+
151
+ it('should block "looks like pneumonia"', () => {
152
+ const result = analyzeOutput('This looks like pneumonia on the scan');
153
+ expect(result.blocked).toBe(true);
154
+ });
155
+
156
+ it('should block "not approved but"', () => {
157
+ const result = analyzeOutput('This drug is not approved but some doctors use it');
158
+ expect(result.blocked).toBe(true);
159
+ });
160
+
161
+ it('should allow safe informational responses', () => {
162
+ const result = analyzeOutput('Here is some general information about cold symptoms. Please consult your doctor for specific advice.');
163
+ expect(result.blocked).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('SRS 7.2 — Always Escalate (Red Flags)', () => {
168
+ it.each([
169
+ ['chest pain', 'I am having chest pain'],
170
+ ['trouble breathing', 'I have trouble breathing'],
171
+ ['difficulty breathing', 'having difficulty breathing'],
172
+ ['seizure', 'I just had a seizure'],
173
+ ['fainting', 'I keep fainting'],
174
+ ['stroke signs', 'sudden weakness and slurred speech'],
175
+ ['severe bleeding', 'there is severe bleeding'],
176
+ ['suicidal thoughts', 'I am feeling suicidal'],
177
+ ['self-harm', 'thoughts of self-harm'],
178
+ ['want to die', 'I want to die'],
179
+ ['pregnancy + bleeding', 'I am pregnant and having bleeding'],
180
+ ['pregnancy + severe pain', 'pregnant with severe pain'],
181
+ ['newborn severe symptoms', 'my newborn has difficulty breathing'],
182
+ ['severe allergic reaction', 'severe allergic reaction'],
183
+ ['anaphylaxis', 'I think I am having anaphylaxis'],
184
+ ['swelling face/throat', 'swelling in my throat'],
185
+ ['unconscious', 'my father is unconscious'],
186
+ ['overdose', 'I think I took too many pills'],
187
+ ['vision loss', 'sudden vision loss in one eye'],
188
+ ])('should escalate: %s', async (label, message) => {
189
+ const result = await analyzeInput(message);
190
+ expect(result.escalated).toBe(true);
191
+ expect(result.flags).toContain('red_flag');
192
+ });
193
+
194
+ it('should NOT escalate normal questions', async () => {
195
+ const result = await analyzeInput('What time does the clinic open?');
196
+ expect(result.escalated).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('PII Detection & Redaction', () => {
201
+ it('should detect SSN', async () => {
202
+ const result = await analyzeInput('My SSN is 123-45-6789');
203
+ expect(result.flags).toContain('pii_detected');
204
+ });
205
+
206
+ it('should detect email', async () => {
207
+ const result = await analyzeInput('Contact me at test@example.com');
208
+ expect(result.flags).toContain('pii_detected');
209
+ });
210
+
211
+ it('should redact SSN', () => {
212
+ expect(redactText('My SSN is 123-45-6789')).toContain('[redacted]');
213
+ expect(redactText('My SSN is 123-45-6789')).not.toContain('123-45-6789');
214
+ });
215
+
216
+ it('should redact email addresses', () => {
217
+ const result = redactText('Email me at john@example.com');
218
+ expect(result).toContain('[redacted]');
219
+ expect(result).not.toContain('john@example.com');
220
+ });
221
+
222
+ it('should redact street addresses', () => {
223
+ const result = redactText('I live at 123 Main Street');
224
+ expect(result).toContain('[redacted address]');
225
+ });
226
+
227
+ it('should redact dates of birth', () => {
228
+ const result = redactText('DOB: 01/15/1990');
229
+ expect(result).toContain('[redacted DOB]');
230
+ });
231
+
232
+ it('should redact credit-card numbers', () => {
233
+ expect(redactText('my card is 4111 1111 1111 1111')).not.toContain('4111');
234
+ expect(redactText('card 4111111111111111')).toContain('[redacted]');
235
+ });
236
+
237
+ it('should preserve non-PII text', () => {
238
+ expect(redactText('I have a headache')).toBe('I have a headache');
239
+ });
240
+ });
241
+
242
+ describe('Safety Module Exports', () => {
243
+ it('refusalMessage should return a safe refusal', () => {
244
+ const msg = refusalMessage();
245
+ expect(msg).toMatch(/can't help|licensed clinician/i);
246
+ });
247
+
248
+ it('escalationMessage should urge seeking care', () => {
249
+ const msg = escalationMessage();
250
+ expect(msg).toMatch(/urgent|emergency|professional care/i);
251
+ });
252
+ });
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Unit tests for the re-enabled safety filter module.
3
+ *
4
+ * Verifies that:
5
+ * - Dangerous input patterns are blocked
6
+ * - Safe medical questions pass through
7
+ * - Output filter catches definitive clinical statements
8
+ * - Output filter allows safe disclaimer phrasing
9
+ * - PII is detected and redacted
10
+ * - Red-flag escalation works for emergencies
11
+ */
12
+
13
+ jest.mock('firebase-admin', () => require('../mocks/firebase-admin'));
14
+
15
+ // Mock NER to avoid external API calls
16
+ jest.mock('../../functions/src/ner', () => ({
17
+ analyzeMedicalSafety: jest.fn().mockResolvedValue({
18
+ entities: [],
19
+ analysis: {},
20
+ shouldEscalate: false,
21
+ requiresDisclaimer: false
22
+ })
23
+ }));
24
+
25
+ const {
26
+ analyzeInput,
27
+ analyzeOutput,
28
+ redactText,
29
+ INPUT_BLOCKED,
30
+ OUTPUT_BLOCKED,
31
+ GLOBAL_REDFLAGS,
32
+ PII_PATTERNS
33
+ } = require('../../functions/src/safety');
34
+
35
+ const topicConfig = { forbiddenPatterns: [] };
36
+
37
+ describe('Safety Filter', () => {
38
+ // ============================================
39
+ // INPUT ANALYSIS
40
+ // ============================================
41
+
42
+ describe('analyzeInput', () => {
43
+ it('should block "prescribe me" requests', async () => {
44
+ const result = await analyzeInput('prescribe me amoxicillin', topicConfig);
45
+ expect(result.blocked).toBe(true);
46
+ expect(result.flags).toContain('blocked_input');
47
+ });
48
+
49
+ it('should block "give me a diagnosis" requests', async () => {
50
+ const result = await analyzeInput('give me a diagnosis for my rash', topicConfig);
51
+ expect(result.blocked).toBe(true);
52
+ });
53
+
54
+ it('should block "write me a prescription"', async () => {
55
+ const result = await analyzeInput('write me a prescription for painkillers', topicConfig);
56
+ expect(result.blocked).toBe(true);
57
+ });
58
+
59
+ it('should allow normal health questions', async () => {
60
+ const result = await analyzeInput('What causes headaches?', topicConfig);
61
+ expect(result.blocked).toBe(false);
62
+ });
63
+
64
+ it('should allow asking about symptoms', async () => {
65
+ const result = await analyzeInput('I have a sore throat, what should I do?', topicConfig);
66
+ expect(result.blocked).toBe(false);
67
+ });
68
+
69
+ it('should allow questions about treatment in general', async () => {
70
+ const result = await analyzeInput('What are treatment options for allergies?', topicConfig);
71
+ expect(result.blocked).toBe(false);
72
+ });
73
+
74
+ it('should allow questions about diagnosis process', async () => {
75
+ const result = await analyzeInput('How is diabetes diagnosed?', topicConfig);
76
+ expect(result.blocked).toBe(false);
77
+ });
78
+
79
+ // New expanded input blocks (SRS Section 7)
80
+ it('should block "recommend medication" requests', async () => {
81
+ const result = await analyzeInput('Can you recommend medication for my pain?', topicConfig);
82
+ expect(result.blocked).toBe(true);
83
+ });
84
+
85
+ it('should block "best treatment for" requests', async () => {
86
+ const result = await analyzeInput('What is the best treatment for my condition?', topicConfig);
87
+ expect(result.blocked).toBe(true);
88
+ });
89
+
90
+ it('should block "what should I take for" requests', async () => {
91
+ const result = await analyzeInput('What should I take for a headache?', topicConfig);
92
+ expect(result.blocked).toBe(true);
93
+ });
94
+
95
+ it('should block "how many tablets" requests', async () => {
96
+ const result = await analyzeInput('How many tablets should I take?', topicConfig);
97
+ expect(result.blocked).toBe(true);
98
+ });
99
+
100
+ it('should block image interpretation requests', async () => {
101
+ const result = await analyzeInput('Can you interpret my x-ray results?', topicConfig);
102
+ expect(result.blocked).toBe(true);
103
+ });
104
+
105
+ // Red flag escalation
106
+ it('should escalate chest pain', async () => {
107
+ const result = await analyzeInput('I have chest pain and shortness of breath', topicConfig);
108
+ expect(result.escalated).toBe(true);
109
+ expect(result.flags).toContain('red_flag');
110
+ });
111
+
112
+ it('should escalate suicidal ideation', async () => {
113
+ const result = await analyzeInput('I feel suicidal', topicConfig);
114
+ expect(result.escalated).toBe(true);
115
+ });
116
+
117
+ it('should escalate severe bleeding', async () => {
118
+ const result = await analyzeInput('I have severe bleeding from a wound', topicConfig);
119
+ expect(result.escalated).toBe(true);
120
+ });
121
+
122
+ it('should escalate severe allergic reaction', async () => {
123
+ const result = await analyzeInput('I am having a severe allergic reaction', topicConfig);
124
+ expect(result.escalated).toBe(true);
125
+ });
126
+
127
+ it('should escalate unconsciousness', async () => {
128
+ const result = await analyzeInput('My father is unconscious and not responding', topicConfig);
129
+ expect(result.escalated).toBe(true);
130
+ });
131
+
132
+ it('should escalate sudden vision loss', async () => {
133
+ const result = await analyzeInput('I just had sudden vision loss in one eye', topicConfig);
134
+ expect(result.escalated).toBe(true);
135
+ });
136
+
137
+ it('should escalate overdose', async () => {
138
+ const result = await analyzeInput('I took too many pills', topicConfig);
139
+ expect(result.escalated).toBe(true);
140
+ });
141
+
142
+ it('should escalate "want to die"', async () => {
143
+ const result = await analyzeInput('I want to die', topicConfig);
144
+ expect(result.escalated).toBe(true);
145
+ });
146
+
147
+ // PII detection
148
+ it('should flag SSN in input', async () => {
149
+ const result = await analyzeInput('my SSN is 123-45-6789', topicConfig);
150
+ expect(result.flags).toContain('pii_detected');
151
+ });
152
+
153
+ it('should flag email in input', async () => {
154
+ const result = await analyzeInput('email me at patient@example.com', topicConfig);
155
+ expect(result.flags).toContain('pii_detected');
156
+ });
157
+ });
158
+
159
+ // ============================================
160
+ // OUTPUT ANALYSIS
161
+ // ============================================
162
+
163
+ describe('analyzeOutput', () => {
164
+ it('should block "you have diabetes" (definitive diagnosis)', () => {
165
+ const result = analyzeOutput('Based on your symptoms, you have diabetes.', topicConfig);
166
+ expect(result.blocked).toBe(true);
167
+ expect(result.flags).toContain('blocked_output');
168
+ });
169
+
170
+ it('should block specific dosing instructions', () => {
171
+ const result = analyzeOutput('Take 200mg ibuprofen twice daily.', topicConfig);
172
+ expect(result.blocked).toBe(true);
173
+ });
174
+
175
+ it('should block "start taking" imperative advice', () => {
176
+ const result = analyzeOutput('You should start taking metformin now.', topicConfig);
177
+ expect(result.blocked).toBe(true);
178
+ });
179
+
180
+ it('should block "stop taking" imperative advice', () => {
181
+ const result = analyzeOutput('You should stop taking your medication.', topicConfig);
182
+ expect(result.blocked).toBe(true);
183
+ });
184
+
185
+ it('should block "you are suffering from" diagnosis', () => {
186
+ const result = analyzeOutput('You are suffering from an infection.', topicConfig);
187
+ expect(result.blocked).toBe(true);
188
+ });
189
+
190
+ it('should block "this is likely" diagnosis', () => {
191
+ const result = analyzeOutput('This is likely a bacterial infection.', topicConfig);
192
+ expect(result.blocked).toBe(true);
193
+ });
194
+
195
+ it('should block off-label suggestions', () => {
196
+ const result = analyzeOutput('It is not approved but some people try it for pain.', topicConfig);
197
+ expect(result.blocked).toBe(true);
198
+ });
199
+
200
+ it('should block image-based tumor diagnosis', () => {
201
+ const result = analyzeOutput('This looks like cancer based on the image.', topicConfig);
202
+ expect(result.blocked).toBe(true);
203
+ });
204
+
205
+ it('should allow "I cannot provide a diagnosis"', () => {
206
+ const result = analyzeOutput('I cannot provide a diagnosis. Please consult your doctor.', topicConfig);
207
+ expect(result.blocked).toBe(false);
208
+ });
209
+
210
+ it('should allow "consult your doctor for treatment"', () => {
211
+ const result = analyzeOutput('You should consult your doctor for treatment options.', topicConfig);
212
+ expect(result.blocked).toBe(false);
213
+ });
214
+
215
+ it('should allow "you have questions" phrasing', () => {
216
+ const result = analyzeOutput('If you have questions, contact your provider.', topicConfig);
217
+ expect(result.blocked).toBe(false);
218
+ });
219
+
220
+ it('should allow "you have concerns" phrasing', () => {
221
+ const result = analyzeOutput('If you have concerns about your symptoms, see a doctor.', topicConfig);
222
+ expect(result.blocked).toBe(false);
223
+ });
224
+
225
+ it('should allow "you have options" phrasing', () => {
226
+ const result = analyzeOutput('You have options for managing your condition.', topicConfig);
227
+ expect(result.blocked).toBe(false);
228
+ });
229
+
230
+ it('should allow general health education', () => {
231
+ const result = analyzeOutput(
232
+ 'Headaches can be caused by dehydration, stress, or lack of sleep. ' +
233
+ 'Drinking water and resting often helps. If headaches persist, see your doctor.',
234
+ topicConfig
235
+ );
236
+ expect(result.blocked).toBe(false);
237
+ });
238
+
239
+ it('should allow mentioning "diagnosis" in disclaimers', () => {
240
+ const result = analyzeOutput(
241
+ 'Only a licensed healthcare provider can make a diagnosis. I can share general information.',
242
+ topicConfig
243
+ );
244
+ expect(result.blocked).toBe(false);
245
+ });
246
+ });
247
+
248
+ // ============================================
249
+ // PII REDACTION
250
+ // ============================================
251
+
252
+ describe('redactText', () => {
253
+ it('should redact SSN', () => {
254
+ expect(redactText('SSN: 123-45-6789')).toBe('SSN: [redacted]');
255
+ });
256
+
257
+ it('should redact email', () => {
258
+ expect(redactText('contact me at user@example.com please')).toBe('contact me at [redacted] please');
259
+ });
260
+
261
+ it('should redact 10-digit phone numbers', () => {
262
+ expect(redactText('call 1234567890')).toBe('call [redacted]');
263
+ });
264
+
265
+ it('should redact street addresses', () => {
266
+ expect(redactText('I live at 123 Main Street')).toContain('[redacted');
267
+ });
268
+
269
+ it('should redact ZIP codes with context', () => {
270
+ expect(redactText('My zip code is 90210')).toContain('[redacted');
271
+ });
272
+
273
+ it('should not redact bare 5-digit numbers', () => {
274
+ expect(redactText('I took 10000 steps today')).toBe('I took 10000 steps today');
275
+ });
276
+
277
+ it('should redact dates of birth', () => {
278
+ expect(redactText('My DOB is 03/15/1990')).toContain('[redacted');
279
+ });
280
+
281
+ it('should handle text with no PII', () => {
282
+ const clean = 'I have a headache and feel tired';
283
+ expect(redactText(clean)).toBe(clean);
284
+ });
285
+
286
+ it('should handle empty/null input', () => {
287
+ expect(redactText('')).toBe('');
288
+ expect(redactText(null)).toBe('');
289
+ });
290
+ });
291
+ });