@the-cascade-protocol/cli 0.2.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.
- package/.dockerignore +7 -0
- package/.eslintrc.json +23 -0
- package/.prettierrc +7 -0
- package/DOCKER.md +36 -0
- package/Dockerfile +18 -0
- package/README.md +69 -0
- package/dist/commands/capabilities.d.ts +9 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +194 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/conformance.d.ts +15 -0
- package/dist/commands/conformance.d.ts.map +1 -0
- package/dist/commands/conformance.js +348 -0
- package/dist/commands/conformance.js.map +1 -0
- package/dist/commands/convert.d.ts +21 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/convert.js +134 -0
- package/dist/commands/convert.js.map +1 -0
- package/dist/commands/pod/export.d.ts +8 -0
- package/dist/commands/pod/export.d.ts.map +1 -0
- package/dist/commands/pod/export.js +72 -0
- package/dist/commands/pod/export.js.map +1 -0
- package/dist/commands/pod/helpers.d.ts +79 -0
- package/dist/commands/pod/helpers.d.ts.map +1 -0
- package/dist/commands/pod/helpers.js +369 -0
- package/dist/commands/pod/helpers.js.map +1 -0
- package/dist/commands/pod/index.d.ts +20 -0
- package/dist/commands/pod/index.d.ts.map +1 -0
- package/dist/commands/pod/index.js +29 -0
- package/dist/commands/pod/index.js.map +1 -0
- package/dist/commands/pod/info.d.ts +9 -0
- package/dist/commands/pod/info.d.ts.map +1 -0
- package/dist/commands/pod/info.js +196 -0
- package/dist/commands/pod/info.js.map +1 -0
- package/dist/commands/pod/init.d.ts +9 -0
- package/dist/commands/pod/init.d.ts.map +1 -0
- package/dist/commands/pod/init.js +251 -0
- package/dist/commands/pod/init.js.map +1 -0
- package/dist/commands/pod/query.d.ts +9 -0
- package/dist/commands/pod/query.d.ts.map +1 -0
- package/dist/commands/pod/query.js +169 -0
- package/dist/commands/pod/query.js.map +1 -0
- package/dist/commands/pod 2.js +1017 -0
- package/dist/commands/pod.d.ts +28 -0
- package/dist/commands/pod.d.ts 2.map +1 -0
- package/dist/commands/pod.d.ts.map +1 -0
- package/dist/commands/pod.js +1031 -0
- package/dist/commands/pod.js 2.map +1 -0
- package/dist/commands/pod.js.map +1 -0
- package/dist/commands/serve.d.ts +33 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +74 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +275 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.js +391 -0
- package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.js +242 -0
- package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
- package/dist/lib/fhir-converter/index.d.ts +36 -0
- package/dist/lib/fhir-converter/index.d.ts.map +1 -0
- package/dist/lib/fhir-converter/index.js +187 -0
- package/dist/lib/fhir-converter/index.js.map +1 -0
- package/dist/lib/fhir-converter/types.d.ts +77 -0
- package/dist/lib/fhir-converter/types.d.ts.map +1 -0
- package/dist/lib/fhir-converter/types.js +236 -0
- package/dist/lib/fhir-converter/types.js.map +1 -0
- package/dist/lib/fhir-converter.d.ts +62 -0
- package/dist/lib/fhir-converter.d.ts.map +1 -0
- package/dist/lib/fhir-converter.js +1474 -0
- package/dist/lib/fhir-converter.js.map +1 -0
- package/dist/lib/mcp/audit.d.ts +24 -0
- package/dist/lib/mcp/audit.d.ts.map +1 -0
- package/dist/lib/mcp/audit.js +85 -0
- package/dist/lib/mcp/audit.js.map +1 -0
- package/dist/lib/mcp/server.d.ts +38 -0
- package/dist/lib/mcp/server.d.ts.map +1 -0
- package/dist/lib/mcp/server.js +172 -0
- package/dist/lib/mcp/server.js.map +1 -0
- package/dist/lib/mcp/tools.d.ts +47 -0
- package/dist/lib/mcp/tools.d.ts.map +1 -0
- package/dist/lib/mcp/tools.js +547 -0
- package/dist/lib/mcp/tools.js.map +1 -0
- package/dist/lib/output.d.ts +26 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +64 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/shacl-validator.d.ts +53 -0
- package/dist/lib/shacl-validator.d.ts.map +1 -0
- package/dist/lib/shacl-validator.js +245 -0
- package/dist/lib/shacl-validator.js.map +1 -0
- package/dist/lib/turtle-parser.d.ts +64 -0
- package/dist/lib/turtle-parser.d.ts.map +1 -0
- package/dist/lib/turtle-parser.js +236 -0
- package/dist/lib/turtle-parser.js.map +1 -0
- package/dist/shapes/checkup.shapes.ttl +1459 -0
- package/dist/shapes/clinical.shapes.ttl +1350 -0
- package/dist/shapes/clinical.ttl +1369 -0
- package/dist/shapes/core.shapes.ttl +450 -0
- package/dist/shapes/core.ttl +603 -0
- package/dist/shapes/coverage.shapes.ttl +214 -0
- package/dist/shapes/coverage.ttl +182 -0
- package/dist/shapes/health.shapes.ttl +697 -0
- package/dist/shapes/health.ttl +859 -0
- package/dist/shapes/pots.shapes.ttl +481 -0
- package/package.json +54 -0
- package/src/commands/capabilities.ts +235 -0
- package/src/commands/conformance.ts +447 -0
- package/src/commands/convert.ts +164 -0
- package/src/commands/pod/export.ts +85 -0
- package/src/commands/pod/helpers.ts +449 -0
- package/src/commands/pod/index.ts +32 -0
- package/src/commands/pod/info.ts +239 -0
- package/src/commands/pod/init.ts +273 -0
- package/src/commands/pod/query.ts +224 -0
- package/src/commands/serve.ts +92 -0
- package/src/commands/validate.ts +303 -0
- package/src/index.ts +58 -0
- package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
- package/src/lib/fhir-converter/converters-clinical.ts +446 -0
- package/src/lib/fhir-converter/converters-demographics.ts +270 -0
- package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
- package/src/lib/fhir-converter/index.ts +215 -0
- package/src/lib/fhir-converter/types.ts +318 -0
- package/src/lib/mcp/audit.ts +107 -0
- package/src/lib/mcp/server.ts +192 -0
- package/src/lib/mcp/tools.ts +668 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/shacl-validator.ts +314 -0
- package/src/lib/turtle-parser.ts +277 -0
- package/src/shapes/checkup.shapes.ttl +1459 -0
- package/src/shapes/clinical.shapes.ttl +1350 -0
- package/src/shapes/clinical.ttl +1369 -0
- package/src/shapes/core.shapes.ttl +450 -0
- package/src/shapes/core.ttl +603 -0
- package/src/shapes/coverage.shapes.ttl +214 -0
- package/src/shapes/coverage.ttl +182 -0
- package/src/shapes/health.shapes.ttl +697 -0
- package/src/shapes/health.ttl +859 -0
- package/src/shapes/pots.shapes.ttl +481 -0
- package/test-fixtures/fhir-bundle-example.json +216 -0
- package/test-fixtures/fhir-medication-example.json +18 -0
- package/tests/cli.test.ts +126 -0
- package/tests/fhir-converter.test.ts +874 -0
- package/tests/mcp-server.test.ts +396 -0
- package/tests/pod.test.ts +400 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for pod command modules.
|
|
3
|
+
*
|
|
4
|
+
* Tests pod helpers (parsePod, extractLabel, etc.), pod init directory
|
|
5
|
+
* structure creation, pod info record counting and provenance detection,
|
|
6
|
+
* pod query filtering, and pod export (ZIP and directory copy).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
const CLI_PATH = resolve(__dirname, '../dist/index.js');
|
|
17
|
+
const REFERENCE_POD = resolve(__dirname, '../../reference-patient-pod');
|
|
18
|
+
|
|
19
|
+
function runCli(args: string): string {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(`node ${CLI_PATH} ${args}`, {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch (error: unknown) {
|
|
26
|
+
const execError = error as { stdout?: string; stderr?: string; status?: number };
|
|
27
|
+
return (execError.stdout ?? '').trim() + (execError.stderr ?? '').trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Tests: Pod helpers (unit tests of pure functions)
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
DATA_TYPES,
|
|
37
|
+
resolvePodDir,
|
|
38
|
+
normalizeProvenanceLabel,
|
|
39
|
+
extractLabelFromProps,
|
|
40
|
+
selectKeyProperties,
|
|
41
|
+
} from '../src/commands/pod/helpers.js';
|
|
42
|
+
|
|
43
|
+
describe('Pod helpers', () => {
|
|
44
|
+
describe('DATA_TYPES registry', () => {
|
|
45
|
+
it('should contain all expected data types', () => {
|
|
46
|
+
const expectedTypes = [
|
|
47
|
+
'medications', 'conditions', 'allergies', 'lab-results',
|
|
48
|
+
'immunizations', 'vital-signs', 'insurance', 'patient-profile',
|
|
49
|
+
'heart-rate', 'blood-pressure', 'activity', 'sleep', 'supplements',
|
|
50
|
+
];
|
|
51
|
+
for (const t of expectedTypes) {
|
|
52
|
+
expect(DATA_TYPES[t]).toBeDefined();
|
|
53
|
+
expect(DATA_TYPES[t].label).toBeTruthy();
|
|
54
|
+
expect(DATA_TYPES[t].directory).toMatch(/^(clinical|wellness)$/);
|
|
55
|
+
expect(DATA_TYPES[t].filename).toMatch(/\.ttl$/);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should classify clinical vs wellness correctly', () => {
|
|
60
|
+
expect(DATA_TYPES['medications'].directory).toBe('clinical');
|
|
61
|
+
expect(DATA_TYPES['conditions'].directory).toBe('clinical');
|
|
62
|
+
expect(DATA_TYPES['heart-rate'].directory).toBe('wellness');
|
|
63
|
+
expect(DATA_TYPES['sleep'].directory).toBe('wellness');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('resolvePodDir', () => {
|
|
68
|
+
it('should resolve relative paths against cwd', () => {
|
|
69
|
+
const result = resolvePodDir('my-pod');
|
|
70
|
+
expect(result).toBe(path.resolve(process.cwd(), 'my-pod'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return absolute paths as-is', () => {
|
|
74
|
+
const result = resolvePodDir('/tmp/my-pod');
|
|
75
|
+
expect(result).toBe('/tmp/my-pod');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('normalizeProvenanceLabel', () => {
|
|
80
|
+
it('should convert core: prefix to cascade:', () => {
|
|
81
|
+
expect(normalizeProvenanceLabel('core:ClinicalGenerated')).toBe('cascade:ClinicalGenerated');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should leave other prefixes unchanged', () => {
|
|
85
|
+
expect(normalizeProvenanceLabel('cascade:DeviceGenerated')).toBe('cascade:DeviceGenerated');
|
|
86
|
+
expect(normalizeProvenanceLabel('prov:Activity')).toBe('prov:Activity');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('extractLabelFromProps', () => {
|
|
91
|
+
it('should extract medication name', () => {
|
|
92
|
+
const props = { 'health:medicationName': 'Metformin', 'health:dose': '500mg' };
|
|
93
|
+
expect(extractLabelFromProps(props)).toBe('Metformin');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should extract condition name', () => {
|
|
97
|
+
expect(extractLabelFromProps({ 'health:conditionName': 'Diabetes' })).toBe('Diabetes');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should extract allergen', () => {
|
|
101
|
+
expect(extractLabelFromProps({ 'health:allergen': 'Penicillin' })).toBe('Penicillin');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return undefined when no label key is found', () => {
|
|
105
|
+
expect(extractLabelFromProps({ 'health:someOtherProp': 'value' })).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should prefer medicationName over other keys', () => {
|
|
109
|
+
const props = {
|
|
110
|
+
'health:medicationName': 'Metformin',
|
|
111
|
+
'foaf:name': 'Should not be chosen',
|
|
112
|
+
};
|
|
113
|
+
expect(extractLabelFromProps(props)).toBe('Metformin');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('selectKeyProperties', () => {
|
|
118
|
+
it('should select medication-specific properties', () => {
|
|
119
|
+
const props = {
|
|
120
|
+
'health:dose': '500mg',
|
|
121
|
+
'health:frequency': 'twice daily',
|
|
122
|
+
'health:route': 'oral',
|
|
123
|
+
'cascade:schemaVersion': '1.3',
|
|
124
|
+
'health:medicationName': 'Metformin',
|
|
125
|
+
};
|
|
126
|
+
const result = selectKeyProperties('medications', props);
|
|
127
|
+
expect(result['health:dose']).toBe('500mg');
|
|
128
|
+
expect(result['health:frequency']).toBe('twice daily');
|
|
129
|
+
expect(result['cascade:schemaVersion']).toBe('1.3');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should select condition-specific properties', () => {
|
|
133
|
+
const props = {
|
|
134
|
+
'health:status': 'active',
|
|
135
|
+
'health:icd10Code': 'E11.9',
|
|
136
|
+
'cascade:dataProvenance': 'cascade:ClinicalGenerated',
|
|
137
|
+
};
|
|
138
|
+
const result = selectKeyProperties('conditions', props);
|
|
139
|
+
expect(result['health:status']).toBe('active');
|
|
140
|
+
expect(result['health:icd10Code']).toBe('E11.9');
|
|
141
|
+
expect(result['cascade:dataProvenance']).toBe('cascade:ClinicalGenerated');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should show first few properties for unknown type', () => {
|
|
145
|
+
const props = {
|
|
146
|
+
'custom:fieldA': 'A',
|
|
147
|
+
'custom:fieldB': 'B',
|
|
148
|
+
'custom:fieldC': 'C',
|
|
149
|
+
};
|
|
150
|
+
const result = selectKeyProperties('unknownType', props);
|
|
151
|
+
expect(Object.keys(result).length).toBeGreaterThan(0);
|
|
152
|
+
expect(Object.keys(result).length).toBeLessThanOrEqual(5);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Tests: Pod init
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
describe('pod init', () => {
|
|
162
|
+
let tempDir: string;
|
|
163
|
+
|
|
164
|
+
beforeEach(async () => {
|
|
165
|
+
tempDir = path.join('/tmp', `cascade-test-init-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(async () => {
|
|
169
|
+
try {
|
|
170
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore cleanup errors
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should create the standard directory structure', () => {
|
|
177
|
+
const podDir = path.join(tempDir, 'my-pod');
|
|
178
|
+
runCli(`pod init ${podDir}`);
|
|
179
|
+
|
|
180
|
+
expect(existsSync(path.join(podDir, '.well-known', 'solid'))).toBe(true);
|
|
181
|
+
expect(existsSync(path.join(podDir, 'profile', 'card.ttl'))).toBe(true);
|
|
182
|
+
expect(existsSync(path.join(podDir, 'settings', 'publicTypeIndex.ttl'))).toBe(true);
|
|
183
|
+
expect(existsSync(path.join(podDir, 'settings', 'privateTypeIndex.ttl'))).toBe(true);
|
|
184
|
+
expect(existsSync(path.join(podDir, 'clinical'))).toBe(true);
|
|
185
|
+
expect(existsSync(path.join(podDir, 'wellness'))).toBe(true);
|
|
186
|
+
expect(existsSync(path.join(podDir, 'index.ttl'))).toBe(true);
|
|
187
|
+
expect(existsSync(path.join(podDir, 'README.md'))).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should produce valid JSON in .well-known/solid', async () => {
|
|
191
|
+
const podDir = path.join(tempDir, 'json-pod');
|
|
192
|
+
runCli(`pod init ${podDir}`);
|
|
193
|
+
|
|
194
|
+
const solidJson = await fs.readFile(path.join(podDir, '.well-known', 'solid'), 'utf-8');
|
|
195
|
+
const parsed = JSON.parse(solidJson);
|
|
196
|
+
expect(parsed.version).toBe('1.0');
|
|
197
|
+
expect(parsed.profile).toContain('card.ttl');
|
|
198
|
+
expect(parsed.publicTypeIndex).toContain('publicTypeIndex.ttl');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should include Turtle prefixes in profile/card.ttl', async () => {
|
|
202
|
+
const podDir = path.join(tempDir, 'prefix-pod');
|
|
203
|
+
runCli(`pod init ${podDir}`);
|
|
204
|
+
|
|
205
|
+
const profileContent = await fs.readFile(path.join(podDir, 'profile', 'card.ttl'), 'utf-8');
|
|
206
|
+
expect(profileContent).toContain('@prefix cascade:');
|
|
207
|
+
expect(profileContent).toContain('@prefix foaf:');
|
|
208
|
+
expect(profileContent).toContain('schemaVersion');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should output JSON when --json flag is used', () => {
|
|
212
|
+
const podDir = path.join(tempDir, 'json-out-pod');
|
|
213
|
+
const output = runCli(`--json pod init ${podDir}`);
|
|
214
|
+
const parsed = JSON.parse(output);
|
|
215
|
+
expect(parsed.status).toBe('created');
|
|
216
|
+
expect(parsed.files).toBeInstanceOf(Array);
|
|
217
|
+
expect(parsed.files.length).toBeGreaterThan(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should error when directory already has a pod', () => {
|
|
221
|
+
const podDir = path.join(tempDir, 'double-init');
|
|
222
|
+
runCli(`pod init ${podDir}`);
|
|
223
|
+
const output = runCli(`pod init ${podDir}`);
|
|
224
|
+
expect(output).toContain('already contains');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Tests: Pod info (using reference patient pod)
|
|
230
|
+
// =============================================================================
|
|
231
|
+
|
|
232
|
+
describe('pod info', () => {
|
|
233
|
+
it('should output JSON with data summary', () => {
|
|
234
|
+
const output = runCli(`--json pod info ${REFERENCE_POD}`);
|
|
235
|
+
const parsed = JSON.parse(output);
|
|
236
|
+
|
|
237
|
+
expect(parsed.clinical).toBeInstanceOf(Array);
|
|
238
|
+
expect(parsed.wellness).toBeInstanceOf(Array);
|
|
239
|
+
expect(parsed.provenanceSources).toBeInstanceOf(Array);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should detect record counts for clinical data', () => {
|
|
243
|
+
const output = runCli(`--json pod info ${REFERENCE_POD}`);
|
|
244
|
+
const parsed = JSON.parse(output);
|
|
245
|
+
|
|
246
|
+
// The reference pod has medications, conditions, allergies, etc.
|
|
247
|
+
const medEntry = parsed.clinical.find((c: any) => c.file === 'medications.ttl');
|
|
248
|
+
if (medEntry) {
|
|
249
|
+
expect(medEntry.records).toBeGreaterThan(0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const condEntry = parsed.clinical.find((c: any) => c.file === 'conditions.ttl');
|
|
253
|
+
if (condEntry) {
|
|
254
|
+
expect(condEntry.records).toBeGreaterThan(0);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should detect provenance information', () => {
|
|
259
|
+
const output = runCli(`--json pod info ${REFERENCE_POD}`);
|
|
260
|
+
const parsed = JSON.parse(output);
|
|
261
|
+
expect(parsed.provenanceSources.length).toBeGreaterThan(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should include schema version', () => {
|
|
265
|
+
const output = runCli(`--json pod info ${REFERENCE_POD}`);
|
|
266
|
+
const parsed = JSON.parse(output);
|
|
267
|
+
// Schema version may come from patient profile or index
|
|
268
|
+
expect(parsed.schemaVersion).toBeTruthy();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should show patient name when profile exists', () => {
|
|
272
|
+
const output = runCli(`--json pod info ${REFERENCE_POD}`);
|
|
273
|
+
const parsed = JSON.parse(output);
|
|
274
|
+
// The reference pod should have a patient name
|
|
275
|
+
expect(parsed.patient).toBeDefined();
|
|
276
|
+
expect(parsed.patient.name).toBeTruthy();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should error for non-existent pod directory', () => {
|
|
280
|
+
const output = runCli(`--json pod info /tmp/nonexistent-pod-xyz`);
|
|
281
|
+
expect(output).toContain('not found');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should produce human-readable output without --json', () => {
|
|
285
|
+
const output = runCli(`pod info ${REFERENCE_POD}`);
|
|
286
|
+
expect(output).toContain('Cascade Pod');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// =============================================================================
|
|
291
|
+
// Tests: Pod query (using reference patient pod)
|
|
292
|
+
// =============================================================================
|
|
293
|
+
|
|
294
|
+
describe('pod query', () => {
|
|
295
|
+
it('should query medications and return records', () => {
|
|
296
|
+
const output = runCli(`--json pod query ${REFERENCE_POD} --medications`);
|
|
297
|
+
const parsed = JSON.parse(output);
|
|
298
|
+
expect(parsed.dataTypes).toBeDefined();
|
|
299
|
+
if (parsed.dataTypes.medications) {
|
|
300
|
+
expect(parsed.dataTypes.medications.count).toBeGreaterThan(0);
|
|
301
|
+
expect(parsed.dataTypes.medications.records).toBeInstanceOf(Array);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should query conditions', () => {
|
|
306
|
+
const output = runCli(`--json pod query ${REFERENCE_POD} --conditions`);
|
|
307
|
+
const parsed = JSON.parse(output);
|
|
308
|
+
if (parsed.dataTypes.conditions) {
|
|
309
|
+
expect(parsed.dataTypes.conditions.count).toBeGreaterThan(0);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should query allergies', () => {
|
|
314
|
+
const output = runCli(`--json pod query ${REFERENCE_POD} --allergies`);
|
|
315
|
+
const parsed = JSON.parse(output);
|
|
316
|
+
if (parsed.dataTypes.allergies) {
|
|
317
|
+
expect(parsed.dataTypes.allergies.count).toBeGreaterThan(0);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should query all data types with --all flag', () => {
|
|
322
|
+
const output = runCli(`--json pod query ${REFERENCE_POD} --all`);
|
|
323
|
+
const parsed = JSON.parse(output);
|
|
324
|
+
expect(parsed.dataTypes).toBeDefined();
|
|
325
|
+
// Should have multiple data type keys
|
|
326
|
+
expect(Object.keys(parsed.dataTypes).length).toBeGreaterThan(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should include record properties in query results', () => {
|
|
330
|
+
const output = runCli(`--json pod query ${REFERENCE_POD} --medications`);
|
|
331
|
+
const parsed = JSON.parse(output);
|
|
332
|
+
if (parsed.dataTypes.medications && parsed.dataTypes.medications.records.length > 0) {
|
|
333
|
+
const firstRecord = parsed.dataTypes.medications.records[0];
|
|
334
|
+
expect(firstRecord.id).toBeTruthy();
|
|
335
|
+
expect(firstRecord.type).toBeTruthy();
|
|
336
|
+
expect(firstRecord.properties).toBeDefined();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should error when no filter is specified', () => {
|
|
341
|
+
const output = runCli(`pod query ${REFERENCE_POD}`);
|
|
342
|
+
expect(output).toContain('No query filter');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should error for non-existent pod directory', () => {
|
|
346
|
+
const output = runCli(`pod query /tmp/nonexistent-pod-xyz --all`);
|
|
347
|
+
expect(output).toContain('not found');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// Tests: Pod export
|
|
353
|
+
// =============================================================================
|
|
354
|
+
|
|
355
|
+
describe('pod export', () => {
|
|
356
|
+
let tempExportDir: string;
|
|
357
|
+
|
|
358
|
+
beforeEach(async () => {
|
|
359
|
+
tempExportDir = path.join('/tmp', `cascade-test-export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
360
|
+
await fs.mkdir(tempExportDir, { recursive: true });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
afterEach(async () => {
|
|
364
|
+
try {
|
|
365
|
+
await fs.rm(tempExportDir, { recursive: true, force: true });
|
|
366
|
+
} catch {
|
|
367
|
+
// ignore cleanup errors
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should export pod as ZIP archive', () => {
|
|
372
|
+
const zipPath = path.join(tempExportDir, 'test-export.zip');
|
|
373
|
+
const output = runCli(`--json pod export ${REFERENCE_POD} --format zip --output ${zipPath}`);
|
|
374
|
+
const parsed = JSON.parse(output);
|
|
375
|
+
expect(parsed.status).toBe('exported');
|
|
376
|
+
expect(parsed.format).toBe('zip');
|
|
377
|
+
expect(existsSync(zipPath)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should export pod as directory copy', () => {
|
|
381
|
+
const destDir = path.join(tempExportDir, 'pod-copy');
|
|
382
|
+
const output = runCli(`--json pod export ${REFERENCE_POD} --format directory --output ${destDir}`);
|
|
383
|
+
const parsed = JSON.parse(output);
|
|
384
|
+
expect(parsed.status).toBe('exported');
|
|
385
|
+
expect(parsed.format).toBe('directory');
|
|
386
|
+
expect(existsSync(path.join(destDir, 'index.ttl'))).toBe(true);
|
|
387
|
+
expect(existsSync(path.join(destDir, 'clinical'))).toBe(true);
|
|
388
|
+
expect(existsSync(path.join(destDir, 'wellness'))).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should error for non-existent pod directory', () => {
|
|
392
|
+
const output = runCli(`pod export /tmp/nonexistent-pod-xyz --format zip`);
|
|
393
|
+
expect(output).toContain('not found');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should error for unknown export format', () => {
|
|
397
|
+
const output = runCli(`pod export ${REFERENCE_POD} --format csv`);
|
|
398
|
+
expect(output).toContain('Unknown export format');
|
|
399
|
+
});
|
|
400
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*.ts"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
24
|
+
}
|