Skip to content

Commit f80e093

Browse files
committed
eventToShortText: checkbox time, test-result labels, convertible display
- Checkbox: show date+time (skip time if midnight) - test-result/scale: Positive/Negative/Indeterminate + percentage (localizable) - medication/basic: show name + dose + route - Convertible: source label + localized method name - Convertible autoConvert: "result (target <- source %)" format - _raw virtual method auto-generated from dimension stops - resolveObservationLabel: resolves raw values to localized labels - HDSSettings._testInject for test-only settings injection - 296 tests passing
1 parent 65c2a17 commit f80e093

5 files changed

Lines changed: 546 additions & 40 deletions

File tree

tests/converterEngine.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,41 @@ describe('[MCVX] HDSModelConverters with loadPack', function () {
274274
converters.loadPack({ engine: 'neural-network', itemKey: 'test', methods: [] });
275275
}, /Unknown converter engine/);
276276
});
277+
278+
it('[MCVX10] creighton→mira round-trip conversions', async () => {
279+
const pairs = [
280+
['0', 'No discharge'], ['6', 'Sticky'], ['8P', 'Creamy'], ['10KL', 'Raw Egg White'],
281+
];
282+
const engine = converters.getEngine('cervical-fluid');
283+
for (const [cr, mira] of pairs) {
284+
const event = await converters.convertMethodToEvent('cervical-fluid', 'creighton', cr);
285+
const result = engine.fromVector('mira', event.content.vectors);
286+
assert.strictEqual(result.data, mira, `Creighton ${cr} → expected ${mira}, got ${result.data}`);
287+
}
288+
});
289+
290+
it('[MCVX11] _raw virtual method is auto-generated', async () => {
291+
const moodEngine = converters.getEngine('mood');
292+
const cfEngine = converters.getEngine('cervical-fluid');
293+
assert.ok(moodEngine.getMethodDef('_raw'), '_raw should exist for mood');
294+
assert.ok(cfEngine.getMethodDef('_raw'), '_raw should exist for cervical-fluid');
295+
assert.strictEqual(moodEngine.getMethodDef('_raw').name.en, 'Raw dimensions');
296+
});
297+
298+
it('[MCVX12] _raw fromVector returns object with stop labels', async () => {
299+
const engine = converters.getEngine('mood');
300+
const result = engine.fromVector('_raw', { valence: 0.5, arousal: 0.5, dominance: 0.5, socialOrientation: 0.5, temporalFocus: 0.5 });
301+
// Exact stops → distance 0
302+
assert.strictEqual(result.matchDistance, 0);
303+
assert.strictEqual(typeof result.data, 'object');
304+
assert.strictEqual(result.data.valence, 0.5);
305+
});
306+
307+
it('[MCVX13] _raw fromVector with non-exact values has non-zero distance', async () => {
308+
const engine = converters.getEngine('mood');
309+
const result = engine.fromVector('_raw', { valence: 0.8, arousal: 0.2, dominance: 0.7, socialOrientation: 0.5, temporalFocus: 0.3 });
310+
assert.ok(result.matchDistance > 0, `Expected non-zero distance, got ${result.matchDistance}`);
311+
const confidence = Math.round((1 - result.matchDistance) * 100);
312+
assert.ok(confidence > 80 && confidence < 100, `Expected partial confidence, got ${confidence}%`);
313+
});
277314
});

tests/eventToShortText.test.js

Lines changed: 274 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { assert } from './test-utils/deps-node.js';
2-
import { HDSModel, eventToShortText } from '../ts/index.ts';
2+
import { eventToShortText } from '../ts/index.ts';
3+
import { getModel } from '../ts/HDSModel/HDSModelInitAndSingleton.ts';
4+
import HDSSettings from '../ts/settings/HDSSettings.ts';
35

46
const modelURL = 'https://model.datasafe.dev/pack.json';
57

68
describe('[ESTX] eventToShortText', () => {
79
let model;
810
before(async () => {
9-
model = new HDSModel(modelURL);
10-
await model.load();
11+
// Use the singleton so eventToShortText picks up the same model instance
12+
model = getModel();
13+
await model.load(modelURL);
1114
});
1215

1316
it('[EST1] returns null for null event', () => {
@@ -48,19 +51,39 @@ describe('[ESTX] eventToShortText', () => {
4851
assert.equal(result, 'Moderate');
4952
});
5053

51-
it('[EST6b] convertible with source block shows source data', () => {
54+
it('[EST6b] convertible with source block shows source data (no autoConvert)', async () => {
55+
await model.converters.ensureEngine('cervical-fluid');
5256
const itemDef = model.itemsDefs.forKey('body-vulva-mucus-inspect');
5357
assert.ok(itemDef, 'itemDef should exist');
5458
const event = {
5559
content: {
56-
data: { threadiness: 0.4, stretchability: 0.3 },
57-
source: { key: 'mira', sourceData: 'Creamy' }
60+
vectors: { threadiness: 0.4, stretchability: 0.3 },
61+
source: { key: 'mira', sourceData: 'Creamy', engineVersion: 'v0', modelVersion: 'v0' }
5862
},
5963
streamIds: [itemDef.data.streamId],
6064
type: itemDef.eventTypes[0]
6165
};
6266
const result = eventToShortText(event);
63-
assert.equal(result, 'Creamy (mira)');
67+
// Without autoConvert setting, shows sourceData + localized method name
68+
assert.equal(result, 'Creamy (Mira)');
69+
});
70+
71+
it('[EST6c] convertible without source shows dimension stop labels', async () => {
72+
// Load converter engine to get dimension labels
73+
await model.converters.ensureEngine('mood');
74+
const event = {
75+
content: {
76+
vectors: { valence: 0.9, arousal: 0.3, dominance: 0.6, socialOrientation: 0, temporalFocus: 0 }
77+
},
78+
streamIds: ['wellbeing-mood'],
79+
type: 'mood/5d-vectors'
80+
};
81+
const result = eventToShortText(event);
82+
// Top 3 dimensions by weight (valence=0.30, arousal=0.25, dominance=0.20)
83+
// nearest stop labels: valence 0.9→"Very pleasant", arousal 0.3→"Calm", dominance 0.6→"Neutral"
84+
// _raw method computes confidence from weighted distance to nearest stops
85+
assert.ok(result.startsWith('Very pleasant, Calm, Neutral'), `Expected start, got: ${result}`);
86+
assert.ok(result.includes('%'), `Expected confidence %, got: ${result}`);
6487
});
6588

6689
it('[EST7] date item returns ISO date string', () => {
@@ -148,11 +171,21 @@ describe('[ESTX] eventToShortText', () => {
148171
assert.ok(result.includes('200'), `Expected 200 in: ${result}`);
149172
});
150173

151-
it('[EST16a] checkbox with null content returns date', () => {
174+
it('[EST16a] checkbox with null content returns date+time', () => {
152175
const event = { content: null, streamIds: ['fertility-cycles-start'], type: 'activity/plain', time: 1720000000 };
153176
const result = eventToShortText(event);
177+
assert.ok(result, 'Should return a date+time string');
178+
assert.ok(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(result), `Expected date+time, got: ${result}`);
179+
});
180+
181+
it('[EST16a2] checkbox at midnight local returns date only', () => {
182+
// Midnight local time
183+
const d = new Date(2024, 6, 3, 0, 0, 0); // July 3, 2024 00:00 local
184+
const midnight = d.getTime() / 1000;
185+
const event = { content: null, streamIds: ['fertility-cycles-start'], type: 'activity/plain', time: midnight };
186+
const result = eventToShortText(event);
154187
assert.ok(result, 'Should return a date string');
155-
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(result), `Expected ISO date, got: ${result}`);
188+
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(result), `Expected date only, got: ${result}`);
156189
});
157190

158191
it('[EST16b] medication with plain string label (not i18n object)', () => {
@@ -232,4 +265,236 @@ describe('[ESTX] eventToShortText', () => {
232265
assert.ok(result.includes('1/3'), `Expected ratio prefix in: ${result}`);
233266
}
234267
});
268+
269+
it('[EST17a] test-result/scale positive (1)', () => {
270+
const event = { content: 1, streamIds: ['fertility-test-opk'], type: 'test-result/scale' };
271+
assert.equal(eventToShortText(event), 'Positive');
272+
});
273+
274+
it('[EST17b] test-result/scale negative (-1)', () => {
275+
const event = { content: -1, streamIds: ['fertility-test-opk'], type: 'test-result/scale' };
276+
assert.equal(eventToShortText(event), 'Negative');
277+
});
278+
279+
it('[EST17c] test-result/scale indeterminate (0)', () => {
280+
const event = { content: 0, streamIds: ['fertility-test-pregnancy'], type: 'test-result/scale' };
281+
assert.equal(eventToShortText(event), 'Indeterminate');
282+
});
283+
284+
it('[EST17d] test-result/scale partial positive (0.56)', () => {
285+
const event = { content: 0.56, streamIds: ['fertility-test-opk'], type: 'test-result/scale' };
286+
assert.equal(eventToShortText(event), 'Positive 56%');
287+
});
288+
289+
it('[EST17e] test-result/scale partial negative (-0.3)', () => {
290+
const event = { content: -0.3, streamIds: ['fertility-test-opk'], type: 'test-result/scale' };
291+
assert.equal(eventToShortText(event), 'Negative 30%');
292+
});
293+
294+
it('[EST18] medication/basic composite shows name + dose', () => {
295+
const event = {
296+
content: { name: 'Ibuprofen', doseValue: 400, doseUnit: 'mg', route: 'oral' },
297+
streamIds: ['medication-intake'],
298+
type: 'medication/basic'
299+
};
300+
const result = eventToShortText(event);
301+
assert.equal(result, 'Ibuprofen — 400 mg, oral');
302+
});
303+
304+
// ─── Convertible: mood ───────────────────────────────────────────
305+
306+
describe('[EST20] convertible mood', () => {
307+
before(async () => {
308+
await model.converters.ensureEngine('mood');
309+
});
310+
311+
it('[EST20a] mood from mira source — no autoConvert', async () => {
312+
const event = await model.converters.convertMethodToEvent('mood', 'mira', 'Happy');
313+
event.time = Date.now() / 1000;
314+
assert.equal(eventToShortText(event), 'Happy (Mira)');
315+
});
316+
317+
it('[EST20b] mood from mira source — various labels', async () => {
318+
const expected = {
319+
Sad: 'Sad (Mira)',
320+
Excited: 'Excited (Mira)',
321+
Normal: 'Normal (Mira)',
322+
'Anxiety or panic attacks': 'Anxiety or panic attacks (Mira)',
323+
};
324+
for (const [label, exp] of Object.entries(expected)) {
325+
const event = await model.converters.convertMethodToEvent('mood', 'mira', label);
326+
event.time = Date.now() / 1000;
327+
assert.equal(eventToShortText(event), exp, `Failed for: ${label}`);
328+
}
329+
});
330+
331+
it('[EST20c] mood raw vector — uses stop labels sorted by weight + confidence', () => {
332+
const event = {
333+
content: { vectors: { valence: 1.0, arousal: 0.9, dominance: 0.8, socialOrientation: 0.7, temporalFocus: 0.6 } },
334+
streamIds: ['wellbeing-mood'],
335+
type: 'mood/5d-vectors'
336+
};
337+
const result = eventToShortText(event);
338+
// valence(w=0.30)→Very pleasant, arousal(w=0.25)→Very energized, dominance(w=0.20)→In control
339+
assert.ok(result.startsWith('Very pleasant, Very energized, In control'), `Expected start, got: ${result}`);
340+
});
341+
342+
it('[EST20d] mood raw vector — neutral baseline is 100% (exact stops)', () => {
343+
const event = {
344+
content: { vectors: { valence: 0.5, arousal: 0.5, dominance: 0.5, socialOrientation: 0.5, temporalFocus: 0.5 } },
345+
streamIds: ['wellbeing-mood'],
346+
type: 'mood/5d-vectors'
347+
};
348+
// All values match exact stops → 100% → no % shown. All 5 dimensions resolved.
349+
assert.equal(eventToShortText(event), 'Neutral, Moderate, Neutral, Balanced, Present');
350+
});
351+
352+
it('[EST20e] mood raw vector — depressed shows confidence', () => {
353+
const event = {
354+
content: { vectors: { valence: 0.1, arousal: 0.1, dominance: 0.1, socialOrientation: 0.2, temporalFocus: 0.1 } },
355+
streamIds: ['wellbeing-mood'],
356+
type: 'mood/5d-vectors'
357+
};
358+
const result = eventToShortText(event);
359+
assert.ok(result.startsWith('Very unpleasant, Very calm, Powerless'), `Expected start, got: ${result}`);
360+
assert.ok(result.includes('%'), `Expected confidence %, got: ${result}`);
361+
});
362+
});
363+
364+
// ─── Convertible: cervical fluid ─────────────────────────────────
365+
366+
describe('[EST21] convertible cervical fluid', () => {
367+
before(async () => {
368+
await model.converters.ensureEngine('cervical-fluid');
369+
});
370+
371+
it('[EST21a] mucus from mira source — no autoConvert', async () => {
372+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'mira', 'Creamy');
373+
event.time = Date.now() / 1000;
374+
assert.equal(eventToShortText(event), 'Creamy (Mira)');
375+
});
376+
377+
it('[EST21b] mucus from mira source — various labels', async () => {
378+
const labels = ['No discharge', 'Dry', 'Sticky', 'Watery', 'Raw Egg White'];
379+
for (const label of labels) {
380+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'mira', label);
381+
event.time = Date.now() / 1000;
382+
assert.equal(eventToShortText(event), `${label} (Mira)`, `Failed for: ${label}`);
383+
}
384+
});
385+
386+
it('[EST21c] mucus from appleHealth source — label localized', async () => {
387+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'appleHealth', 'eggWhite');
388+
event.time = Date.now() / 1000;
389+
// "eggWhite" value resolves to "Egg White" label
390+
assert.equal(eventToShortText(event), 'Egg White (Apple Health)');
391+
});
392+
393+
it('[EST21d] mucus from creighton source — label localized', async () => {
394+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'creighton', '10KL');
395+
event.time = Date.now() / 1000;
396+
const result = eventToShortText(event);
397+
// Creighton 10KL has a descriptive label in its method definition
398+
assert.ok(result.includes('Creighton Model'), `Expected method name, got: ${result}`);
399+
assert.ok(result.includes('10KL'), `Expected observation value, got: ${result}`);
400+
});
401+
});
402+
403+
// ─── Convertible: autoConvert via eventToShortText ──
404+
405+
describe('[EST22] convertible autoConvert in eventToShortText', () => {
406+
before(async () => {
407+
await model.converters.ensureEngine('mood');
408+
await model.converters.ensureEngine('cervical-fluid');
409+
});
410+
411+
afterEach(() => {
412+
HDSSettings.unhook();
413+
});
414+
415+
it('[EST22a] no autoConvert — shows sourceData + method name', async () => {
416+
const event = await model.converters.convertMethodToEvent('mood', 'mira', 'Excited');
417+
event.time = Date.now() / 1000;
418+
assert.equal(eventToShortText(event), 'Excited (Mira)');
419+
});
420+
421+
it('[EST22b] autoConvert same method — shows sourceData + method name (no conversion)', async () => {
422+
HDSSettings._testInject('autoConvert-wellbeing-mood', 'mira');
423+
const event = await model.converters.convertMethodToEvent('mood', 'mira', 'Happy');
424+
event.time = Date.now() / 1000;
425+
assert.equal(eventToShortText(event), 'Happy (Mira)');
426+
});
427+
428+
it('[EST22c] autoConvert mood mira→hds — shows stop labels with target <- source', async () => {
429+
HDSSettings._testInject('autoConvert-wellbeing-mood', 'hds');
430+
const event = await model.converters.convertMethodToEvent('mood', 'mira', 'Happy');
431+
event.time = Date.now() / 1000;
432+
const result = eventToShortText(event);
433+
assert.ok(result.includes('Pleasant'), `Expected stop label, got: ${result}`);
434+
assert.ok(result.includes('HDS Native <- Mira'), `Expected target <- source, got: ${result}`);
435+
assert.ok(result.includes('%'), `Expected confidence %, got: ${result}`);
436+
});
437+
438+
it('[EST22d] autoConvert cervical fluid mira→appleHealth — localized label + perfect match', async () => {
439+
HDSSettings._testInject('autoConvert-body-vulva-mucus-inspect', 'appleHealth');
440+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'mira', 'Creamy');
441+
event.time = Date.now() / 1000;
442+
// "creamy" value should resolve to "Creamy" label from appleHealth method
443+
assert.equal(eventToShortText(event), 'Creamy (Apple Health <- Mira)');
444+
});
445+
446+
it('[EST22e] autoConvert cervical fluid mira→billings — localized label + partial match', async () => {
447+
HDSSettings._testInject('autoConvert-body-vulva-mucus-inspect', 'billings');
448+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'mira', 'Watery');
449+
event.time = Date.now() / 1000;
450+
const result = eventToShortText(event);
451+
// billings "wetSlippery" value should have a localized label
452+
assert.ok(!result.includes('wetSlippery'), `Should use label not value, got: ${result}`);
453+
assert.ok(result.includes('Billings (BOM) <- Mira'), `Expected target <- source, got: ${result}`);
454+
assert.ok(result.includes('%'), `Expected confidence %, got: ${result}`);
455+
});
456+
457+
it('[EST22f] autoConvert raw vector (no source) — shows result + target name', async () => {
458+
HDSSettings._testInject('autoConvert-wellbeing-mood', 'mira');
459+
const event = {
460+
content: { vectors: { valence: 0.8, arousal: 0.2, dominance: 0.7, socialOrientation: 0.5, temporalFocus: 0.3 } },
461+
streamIds: ['wellbeing-mood'],
462+
type: 'mood/5d-vectors',
463+
time: Date.now() / 1000
464+
};
465+
const result = eventToShortText(event);
466+
assert.ok(result.includes('Mira'), `Expected target method name, got: ${result}`);
467+
assert.ok(result.includes('%'), `Expected confidence %, got: ${result}`);
468+
});
469+
470+
it('[EST22g] all cervical fluid mira→appleHealth labels are capitalized', async () => {
471+
HDSSettings._testInject('autoConvert-body-vulva-mucus-inspect', 'appleHealth');
472+
const pairs = [
473+
['No discharge', 'Dry'], ['Dry', 'Dry'], ['Sticky', 'Sticky'],
474+
['Creamy', 'Creamy'], ['Watery', 'Watery'], ['Raw Egg White', 'Egg White'],
475+
];
476+
for (const [mira, expectedLabel] of pairs) {
477+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'mira', mira);
478+
event.time = Date.now() / 1000;
479+
const result = eventToShortText(event);
480+
assert.ok(result.startsWith(expectedLabel), `${mira} → expected "${expectedLabel}...", got: "${result}"`);
481+
}
482+
});
483+
484+
it('[EST22h] cervical fluid source labels are localized from method definition', async () => {
485+
// appleHealth source "eggWhite" should show as "Egg White" (label), not "eggWhite" (value)
486+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'appleHealth', 'eggWhite');
487+
event.time = Date.now() / 1000;
488+
const result = eventToShortText(event);
489+
assert.equal(result, 'Egg White (Apple Health)');
490+
});
491+
492+
it('[EST22i] cervical fluid creighton source shows localized label', async () => {
493+
const event = await model.converters.convertMethodToEvent('cervical-fluid', 'creighton', '10KL');
494+
event.time = Date.now() / 1000;
495+
const result = eventToShortText(event);
496+
// creighton "10KL" has a label in its method definition
497+
assert.ok(result.includes('Creighton Model'), `Expected method name, got: ${result}`);
498+
});
499+
});
235500
});

0 commit comments

Comments
 (0)