import test from 'node:test'; import assert from 'node:assert/strict'; import { homePreparation } from '../src/components/home/home-model.ts'; const check = (code, status, blocking = true, action) => ({ code, status, blocking, action, title: code, detail: `${code} details` }); const readiness = (checks, extra = {}) => ({ subject_id: 'elder-a', ready: !checks.some(item => item.blocking && item.status !== 'ready'), checks, devices: [], evaluated_at: '2026-10-05T00:00:00Z', start_time_checks: ['usage_quota', 'runtime_routing', 'no_active_conversation'], ...extra }); test('an elder is required before subject-scoped preparation can be checked', () => { const result = homePreparation(undefined, readiness([check('VOICE_CONSENT', 'ready')])); assert.equal(result.status, 'needs_subject'); assert.equal(result.destination, 'subjects'); assert.equal(result.checks.length, 0); }); test('missing, failed and refreshing data never become unfinished setup', () => { const previous = readiness([check('VOICE_CONSENT', 'blocked', true, 'MANAGE_CONSENTS')]); for (const [data, state] of [[undefined, undefined], [previous, 'error'], [previous, 'loading']]) { const result = homePreparation('elder-a', data, state); assert.equal(result.status, 'unknown'); assert.equal(result.pending, 0); assert.equal(result.completed, 0); assert.equal(result.destination, 'retry'); } }); test('changing elder does not reuse another elder preparation', () => { const result = homePreparation('elder-b', readiness([check('SUBJECT_ACTIVE', 'ready')])); assert.equal(result.status, 'unknown'); assert.equal(result.checks.length, 0); }); test('voice authorization has a direct privacy destination even with a ready device', () => { const result = homePreparation('elder-a', readiness([ check('DEVICE_BOUND', 'ready'), check('DEVICE_RUNTIME', 'ready'), check('VOICE_CONSENT', 'blocked', true, 'MANAGE_CONSENTS'), ])); assert.equal(result.destination, 'privacy'); assert.equal(result.next.code, 'VOICE_CONSENT'); assert.equal(result.pending, 1); assert.equal(result.completed, 2); }); test('next action follows remaining server blockers instead of a fixed device-first sequence', () => { const profile = check('PROFILE_BACKGROUND', 'blocked', true, 'EDIT_PROFILE'); const device = check('DEVICE_BOUND', 'blocked', true, 'BIND_DEVICE'); assert.equal(homePreparation('elder-a', readiness([profile, device])).destination, 'subjects'); assert.equal(homePreparation('elder-a', readiness([device, profile])).destination, 'devices'); const result = homePreparation('elder-a', readiness([device, profile])); assert.deepEqual(result.checks.map(item => item.destination), ['devices', 'subjects']); }); test('web binding guidance matches the existing Bluetooth add and assignment flow', () => { const result = homePreparation('elder-a', readiness([{ ...check('DEVICE_BOUND', 'blocked', true, 'BIND_DEVICE'), detail: '请使用设备屏幕显示的一次性绑定码完成绑定。' }])); assert.match(result.next.detail, /蓝牙/); assert.match(result.next.detail, /已添加/); assert.doesNotMatch(result.next.detail, /绑定码|小程序/); assert.equal(result.destination, 'devices'); }); test('attention checks remain optional and do not count as blockers', () => { const result = homePreparation('elder-a', readiness([ check('SUBJECT_ACTIVE', 'ready'), check('VOICE_CONSENT', 'ready'), check('CONFIG_SYNC', 'attention', false, 'OPEN_DEVICES'), check('RECENT_CONTACT', 'attention', false, 'OPEN_DEVICES'), ])); assert.equal(result.status, 'ready'); assert.equal(result.pending, 0); assert.equal(result.suggestions, 2); assert.equal(result.next.blocking, false); assert.equal(result.destination, 'devices'); }); test('a blocker is handled before an earlier optional suggestion', () => { const result = homePreparation('elder-a', readiness([ check('CONFIG_SYNC', 'attention', false, 'OPEN_DEVICES'), check('CONVERSATION_GOAL', 'blocked', true, 'EDIT_PROFILE'), ])); assert.equal(result.next.code, 'CONVERSATION_GOAL'); assert.equal(result.destination, 'subjects'); }); test('completed checks lead to records without inventing memory or recording consent', () => { const result = homePreparation('elder-a', readiness([check('VOICE_CONSENT', 'ready'), check('DEVICE_BOUND', 'ready')])); assert.equal(result.status, 'ready'); assert.equal(result.destination, 'conversations'); assert.equal(result.next, undefined); assert.equal(result.checks.length, 2); }); test('empty or contradictory payloads do not claim completion', () => { for (const data of [readiness([]), readiness([check('VOICE_CONSENT', 'ready')], { ready: false }), readiness([check('VOICE_CONSENT', 'blocked')], { ready: true })]) { assert.equal(homePreparation('elder-a', data).status, 'unknown'); } }); test('an unknown blocking action does not guess a settings destination', () => { const result = homePreparation('elder-a', readiness([check('VOICE_CONSENT', 'blocked', true)])); assert.equal(result.status, 'blocked'); assert.equal(result.destination, 'retry'); assert.equal(result.next.detail, 'VOICE_CONSENT details'); });