|
1 | 1 | 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'; |
3 | 5 |
|
4 | 6 | const modelURL = 'https://model.datasafe.dev/pack.json'; |
5 | 7 |
|
6 | 8 | describe('[ESTX] eventToShortText', () => { |
7 | 9 | let model; |
8 | 10 | 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); |
11 | 14 | }); |
12 | 15 |
|
13 | 16 | it('[EST1] returns null for null event', () => { |
@@ -48,19 +51,39 @@ describe('[ESTX] eventToShortText', () => { |
48 | 51 | assert.equal(result, 'Moderate'); |
49 | 52 | }); |
50 | 53 |
|
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'); |
52 | 56 | const itemDef = model.itemsDefs.forKey('body-vulva-mucus-inspect'); |
53 | 57 | assert.ok(itemDef, 'itemDef should exist'); |
54 | 58 | const event = { |
55 | 59 | 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' } |
58 | 62 | }, |
59 | 63 | streamIds: [itemDef.data.streamId], |
60 | 64 | type: itemDef.eventTypes[0] |
61 | 65 | }; |
62 | 66 | 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}`); |
64 | 87 | }); |
65 | 88 |
|
66 | 89 | it('[EST7] date item returns ISO date string', () => { |
@@ -148,11 +171,21 @@ describe('[ESTX] eventToShortText', () => { |
148 | 171 | assert.ok(result.includes('200'), `Expected 200 in: ${result}`); |
149 | 172 | }); |
150 | 173 |
|
151 | | - it('[EST16a] checkbox with null content returns date', () => { |
| 174 | + it('[EST16a] checkbox with null content returns date+time', () => { |
152 | 175 | const event = { content: null, streamIds: ['fertility-cycles-start'], type: 'activity/plain', time: 1720000000 }; |
153 | 176 | 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); |
154 | 187 | 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}`); |
156 | 189 | }); |
157 | 190 |
|
158 | 191 | it('[EST16b] medication with plain string label (not i18n object)', () => { |
@@ -232,4 +265,236 @@ describe('[ESTX] eventToShortText', () => { |
232 | 265 | assert.ok(result.includes('1/3'), `Expected ratio prefix in: ${result}`); |
233 | 266 | } |
234 | 267 | }); |
| 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 | + }); |
235 | 500 | }); |
0 commit comments