Files
story/web/test/home-preparation.test.mjs
T

102 lines
5.1 KiB
JavaScript
Raw Normal View History

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');
});