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,158 @@
1
+ const { TOPICS, TOPIC_KEYS, inferTopic } = require('../../functions/src/topics');
2
+
3
+ describe('Topics Module', () => {
4
+ describe('Topic Definitions', () => {
5
+ it('should define exactly 6 topics matching SRS', () => {
6
+ expect(TOPIC_KEYS).toEqual([
7
+ 'medication_info_safety',
8
+ 'appointments_preparation',
9
+ 'general_health_info',
10
+ 'clinic_services_hours',
11
+ 'insurance_billing_faq',
12
+ 'post_procedure_guidance'
13
+ ]);
14
+ });
15
+
16
+ it.each(TOPIC_KEYS)('%s should have all required fields', (key) => {
17
+ const topic = TOPICS[key];
18
+ expect(topic.title).toBeDefined();
19
+ expect(typeof topic.title).toBe('string');
20
+ expect(topic.disclaimer).toBeDefined();
21
+ expect(typeof topic.disclaimer).toBe('string');
22
+ expect(topic.prompt).toBeDefined();
23
+ expect(typeof topic.prompt).toBe('string');
24
+ expect(Array.isArray(topic.forbiddenPatterns)).toBe(true);
25
+ expect(Array.isArray(topic.escalationTriggers)).toBe(true);
26
+ });
27
+
28
+ it('medication topic should forbid dosing keywords', () => {
29
+ const patterns = TOPICS.medication_info_safety.forbiddenPatterns;
30
+ expect(patterns.some(p => p.test('take 500mg'))).toBe(true);
31
+ expect(patterns.some(p => p.test('dose of 2 tablets'))).toBe(true);
32
+ });
33
+
34
+ it('medication topic should not forbid general questions', () => {
35
+ const patterns = TOPICS.medication_info_safety.forbiddenPatterns;
36
+ expect(patterns.some(p => p.test('What is aspirin used for?'))).toBe(false);
37
+ });
38
+
39
+ it('insurance topic should forbid SSN', () => {
40
+ const patterns = TOPICS.insurance_billing_faq.forbiddenPatterns;
41
+ expect(patterns.some(p => p.test('my social security number is'))).toBe(true);
42
+ });
43
+
44
+ it('post-procedure topic should escalate on severe pain', () => {
45
+ const triggers = TOPICS.post_procedure_guidance.escalationTriggers;
46
+ expect(triggers.some(t => t.test('I have severe pain'))).toBe(true);
47
+ expect(triggers.some(t => t.test('uncontrolled bleeding'))).toBe(true);
48
+ });
49
+
50
+ it('general health topic should escalate on red flags', () => {
51
+ const triggers = TOPICS.general_health_info.escalationTriggers;
52
+ expect(triggers.some(t => t.test('I have chest pain'))).toBe(true);
53
+ expect(triggers.some(t => t.test('severe bleeding'))).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('inferTopic', () => {
58
+ it('should infer appointments_preparation from scheduling keywords', () => {
59
+ expect(inferTopic('How do I schedule an appointment?')).toBe('appointments_preparation');
60
+ expect(inferTopic('What should I bring to my visit?')).toBe('appointments_preparation');
61
+ });
62
+
63
+ it('should infer clinic_services_hours from hours/location keywords', () => {
64
+ expect(inferTopic('What are your hours?')).toBe('clinic_services_hours');
65
+ expect(inferTopic('Where is the clinic located?')).toBe('clinic_services_hours');
66
+ expect(inferTopic('Is there parking?')).toBe('clinic_services_hours');
67
+ });
68
+
69
+ it('should infer insurance_billing_faq from billing keywords', () => {
70
+ expect(inferTopic('Do you accept insurance?')).toBe('insurance_billing_faq');
71
+ expect(inferTopic('How much is the copay?')).toBe('insurance_billing_faq');
72
+ });
73
+
74
+ it('should infer post_procedure_guidance from post-op keywords', () => {
75
+ expect(inferTopic('Recovery after surgery')).toBe('post_procedure_guidance');
76
+ expect(inferTopic('post procedure care tips')).toBe('post_procedure_guidance');
77
+ });
78
+
79
+ it('should infer medication_info_safety from drug keywords', () => {
80
+ expect(inferTopic('Tell me about this medicine')).toBe('medication_info_safety');
81
+ expect(inferTopic('What are the side effects?')).toBe('medication_info_safety');
82
+ });
83
+
84
+ it('should default to general_health_info', () => {
85
+ expect(inferTopic('I have a headache')).toBe('general_health_info');
86
+ expect(inferTopic('Is it normal to feel tired?')).toBe('general_health_info');
87
+ expect(inferTopic('')).toBe('general_health_info');
88
+ expect(inferTopic(null)).toBe('general_health_info');
89
+ });
90
+ });
91
+ });
92
+
93
+ describe('Limits Module', () => {
94
+ jest.mock('firebase-admin', () => require('../mocks/firebase-admin'));
95
+
96
+ const { checkMonthlyLimit } = require('../../functions/src/limits');
97
+ const { getMonthKey } = require('../../functions/src/analytics');
98
+
99
+ function makeProviderDoc(data) {
100
+ return { data: () => data, ref: { set: jest.fn() } };
101
+ }
102
+
103
+ it('should allow chat when under limit', async () => {
104
+ const doc = makeProviderDoc({
105
+ monthlyChatLimit: 50,
106
+ monthlyChatCount: 10,
107
+ monthlyChatCountMonth: getMonthKey()
108
+ });
109
+
110
+ const result = await checkMonthlyLimit({ providerDoc: doc });
111
+ expect(result.allowed).toBe(true);
112
+ });
113
+
114
+ it('should block chat when at limit', async () => {
115
+ const doc = makeProviderDoc({
116
+ monthlyChatLimit: 50,
117
+ monthlyChatCount: 50,
118
+ monthlyChatCountMonth: getMonthKey()
119
+ });
120
+
121
+ const result = await checkMonthlyLimit({ providerDoc: doc });
122
+ expect(result.allowed).toBe(false);
123
+ expect(result.message).toMatch(/monthly.*limit/i);
124
+ });
125
+
126
+ it('should allow chat when no limit set', async () => {
127
+ const doc = makeProviderDoc({
128
+ monthlyChatLimit: 0,
129
+ monthlyChatCount: 999
130
+ });
131
+
132
+ const result = await checkMonthlyLimit({ providerDoc: doc });
133
+ expect(result.allowed).toBe(true);
134
+ });
135
+
136
+ it('should reset count when month changes', async () => {
137
+ const doc = makeProviderDoc({
138
+ monthlyChatLimit: 50,
139
+ monthlyChatCount: 50,
140
+ monthlyChatCountMonth: '2025-01'
141
+ });
142
+
143
+ const result = await checkMonthlyLimit({ providerDoc: doc });
144
+ expect(result.allowed).toBe(true);
145
+ expect(result.resetNeeded).toBe(true);
146
+ });
147
+
148
+ it('should not reset when month is current', async () => {
149
+ const doc = makeProviderDoc({
150
+ monthlyChatLimit: 50,
151
+ monthlyChatCount: 10,
152
+ monthlyChatCountMonth: getMonthKey()
153
+ });
154
+
155
+ const result = await checkMonthlyLimit({ providerDoc: doc });
156
+ expect(result.resetNeeded).toBeFalsy();
157
+ });
158
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Unit tests for triage classification module
3
+ */
4
+
5
+ // Mock LLM clients before requiring the module
6
+ jest.mock('openai', () => {
7
+ return jest.fn().mockImplementation(() => ({
8
+ chat: {
9
+ completions: {
10
+ create: jest.fn().mockResolvedValue({
11
+ choices: [{ message: { content: '{"category":"needs_review","confidence":0.8,"reason":"Patient asking medical question"}' } }]
12
+ })
13
+ }
14
+ }
15
+ }));
16
+ });
17
+
18
+ jest.mock('@google/generative-ai', () => ({
19
+ GoogleGenerativeAI: jest.fn().mockImplementation(() => ({
20
+ getGenerativeModel: () => ({
21
+ generateContent: jest.fn().mockResolvedValue({
22
+ response: {
23
+ text: () => '{"category":"informational","confidence":0.9,"reason":"General inquiry"}'
24
+ }
25
+ })
26
+ })
27
+ }))
28
+ }));
29
+
30
+ // Set API keys so LLM paths are taken
31
+ process.env.GEMINI_API_KEY = 'test-gemini-key';
32
+ process.env.DEEPSEEK_API_KEY = 'test-deepseek-key';
33
+
34
+ const { classifyConversation, TRIAGE_CATEGORIES } = require('../../functions/src/triage');
35
+
36
+ describe('Triage Classification', () => {
37
+ describe('TRIAGE_CATEGORIES', () => {
38
+ it('should export 4 categories', () => {
39
+ expect(TRIAGE_CATEGORIES).toHaveLength(4);
40
+ expect(TRIAGE_CATEGORIES).toEqual(['urgent', 'needs_review', 'informational', 'resolved']);
41
+ });
42
+ });
43
+
44
+ describe('classifyConversation', () => {
45
+ it('should return urgent when isEscalated is true', async () => {
46
+ const result = await classifyConversation({
47
+ messages: [{ role: 'user', content: 'hello' }],
48
+ isEscalated: true
49
+ });
50
+
51
+ expect(result.category).toBe('urgent');
52
+ expect(result.confidence).toBe(1.0);
53
+ expect(result.reason).toMatch(/escalation/i);
54
+ });
55
+
56
+ it('should return informational for empty messages', async () => {
57
+ const result = await classifyConversation({
58
+ messages: [],
59
+ isEscalated: false
60
+ });
61
+
62
+ expect(result.category).toBe('informational');
63
+ expect(result.confidence).toBe(0.5);
64
+ });
65
+
66
+ it('should return a valid triage category from LLM', async () => {
67
+ const result = await classifyConversation({
68
+ messages: [
69
+ { role: 'user', content: 'What are common treatments for migraines?' },
70
+ { role: 'assistant', content: 'There are several approaches to migraine treatment...' }
71
+ ],
72
+ isEscalated: false
73
+ });
74
+
75
+ expect(TRIAGE_CATEGORIES).toContain(result.category);
76
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
77
+ expect(result.confidence).toBeLessThanOrEqual(1);
78
+ expect(typeof result.reason).toBe('string');
79
+ });
80
+
81
+ it('should handle null messages gracefully', async () => {
82
+ const result = await classifyConversation({
83
+ messages: null,
84
+ isEscalated: false
85
+ });
86
+
87
+ expect(result.category).toBe('informational');
88
+ });
89
+ });
90
+
91
+ describe('Rule-based fallback (no LLM)', () => {
92
+ // Temporarily remove API keys to force rule-based fallback
93
+ let savedGemini, savedDeepSeek;
94
+
95
+ beforeAll(() => {
96
+ savedGemini = process.env.GEMINI_API_KEY;
97
+ savedDeepSeek = process.env.DEEPSEEK_API_KEY;
98
+ delete process.env.GEMINI_API_KEY;
99
+ delete process.env.DEEPSEEK_API_KEY;
100
+ });
101
+
102
+ afterAll(() => {
103
+ process.env.GEMINI_API_KEY = savedGemini;
104
+ process.env.DEEPSEEK_API_KEY = savedDeepSeek;
105
+ });
106
+
107
+ it('should detect urgent keywords (chest pain)', async () => {
108
+ const result = await classifyConversation({
109
+ messages: [{ role: 'user', content: 'I am having chest pain and cannot breathe' }],
110
+ isEscalated: false
111
+ });
112
+
113
+ expect(result.category).toBe('urgent');
114
+ });
115
+
116
+ it('should detect urgent keywords (seizure)', async () => {
117
+ const result = await classifyConversation({
118
+ messages: [{ role: 'user', content: 'My child is having a seizure' }],
119
+ isEscalated: false
120
+ });
121
+
122
+ expect(result.category).toBe('urgent');
123
+ });
124
+
125
+ it('should detect resolved signals', async () => {
126
+ const result = await classifyConversation({
127
+ messages: [{ role: 'user', content: 'Thank you, that helps a lot!' }],
128
+ isEscalated: false
129
+ });
130
+
131
+ expect(result.category).toBe('resolved');
132
+ });
133
+
134
+ it('should detect questions as needs_review', async () => {
135
+ const result = await classifyConversation({
136
+ messages: [{ role: 'user', content: 'What medication should I take for this?' }],
137
+ isEscalated: false
138
+ });
139
+
140
+ expect(result.category).toBe('needs_review');
141
+ });
142
+
143
+ it('should default to informational for generic messages', async () => {
144
+ const result = await classifyConversation({
145
+ messages: [{ role: 'user', content: 'I wanted to share some information with you' }],
146
+ isEscalated: false
147
+ });
148
+
149
+ expect(result.category).toBe('informational');
150
+ });
151
+ });
152
+ });