"""Modo vida (bichinho virtual): necessidades, sono, atos, figurinhas com limite e rotas HTTP.""" import http.client import importlib.util import json import random import sys import tempfile import threading import time import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from vida import Vida # noqa: E402 spec = importlib.util.spec_from_file_location('kiosk_vida', ROOT / 'kiosk-server.py') kiosk = importlib.util.module_from_spec(spec) spec.loader.exec_module(kiosk) DIA = time.mktime((2026, 10, 6, 15, 0, 0, 0, 0, -1)) # 15h: longe da noite class TreinoFalso: def __init__(self): self.pedidos, self.tocadas = [], [] def gerar_figurinha(self, assunto): self.pedidos.append(assunto) def status(self): return dict(reacoes=[dict(id=7, receita=dict(nome='Dança'), feedback=0, ativa=True)]) def testar(self, ident): self.tocadas.append(ident) class VidaTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1)) self.ordens = [] self.v.enfileirar = self.ordens.append self.v.ultimo_tick = DIA def tearDown(self): self.tmp.cleanup() def test_necessidades_caem_e_sono_recupera(self): self.v.necessidades.update(energia=50, social=50, diversao=50) self.v.passar_tempo(DIA + 600) # 10 min acordado self.assertLess(self.v.necessidades['energia'], 50) self.assertLess(self.v.necessidades['diversao'], 50) self.v.dormindo_proprio = True antes = self.v.necessidades['energia'] self.v.passar_tempo(DIA + 1200) # 10 min dormindo self.assertGreater(self.v.necessidades['energia'], antes) def test_pausa_longa_nao_zera(self): self.v.passar_tempo(DIA + 86400) # placa desligada um dia self.assertGreater(self.v.necessidades['energia'], 60) def test_cansado_vai_dormir_e_acorda_descansado(self): self.v.necessidades['energia'] = 10 self.assertEqual(self.v.agir(DIA), 'dormir') self.assertIn('dormir', self.ordens) self.assertTrue(self.v.dormindo()) self.assertIsNone(self.v.agir(DIA + 60)) # ainda sem energia: continua dormindo self.v.necessidades['energia'] = 100 self.assertEqual(self.v.agir(DIA + 120), 'acordar') def test_madrugada_dorme_mesmo_com_gente_se_mexendo(self): noite = time.mktime((2026, 10, 6, 23, 30, 0, 0, 0, -1)) self.v.alguem_perto = lambda: True # deitado na cama, o PIR vê self.assertEqual(self.v.agir(noite), 'dormir') self.v.necessidades['energia'] = 100 self.assertIsNone(self.v.agir(noite + 60)) # descansado, mas é madrugada: não acorda self.v.estimulo('carinho', noite + 120) # carinho acorda e segura acordado um tempo self.assertFalse(self.v.dormindo()) self.assertNotEqual(self.v.agir(noite + 180), 'dormir') self.assertEqual(self.v.agir(noite + 180 + 11 * 60), 'dormir') def test_ritmo_vira_sensibilidade(self): self.v.configurar({'ritmo': 'calmo'}) self.assertIn('ritmo calmo', self.ordens) self.assertFalse(self.v.pir_acorda(DIA)) # calmo: 3 disparos do PIR em 2 min self.assertFalse(self.v.pir_acorda(DIA + 30)) self.assertTrue(self.v.pir_acorda(DIA + 60)) self.assertFalse(self.v.pir_acorda(DIA + 400)) # os antigos expiraram self.v.configurar({'ritmo': 'agitado'}) self.assertTrue(self.v.pir_acorda(DIA + 500)) # agitado: um basta self.ordens.clear() self.v.tick(DIA + 505) self.assertIn('ritmo agitado', self.ordens) # reenviado junto com o humor self.v.configurar({'ligado': False}) self.assertIn('ritmo normal', self.ordens) self.assertTrue(self.v.pir_acorda(DIA + 600)) # desligado: comportamento de sempre def test_horario_da_madrugada(self): from controle_pc import madrugada h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1)) cfg = dict(noiteSilenciosa=True, noiteInicio=23, noiteFim=7) self.assertEqual([madrugada(cfg, h(x)) for x in (22, 23, 3, 6, 7, 12)], [False, True, True, True, False, False]) self.assertTrue(madrugada(dict(cfg, noiteInicio=1, noiteFim=5), h(2))) self.assertFalse(madrugada(dict(cfg, noiteSilenciosa=False), h(3))) def test_humor_e_gestos_de_carente_com_gente_perto(self): self.v.necessidades.update(energia=80, social=10) self.assertEqual(self.v.humor(), 'carente') self.v.alguem_perto = lambda: True gesto = self.v.agir(DIA) self.assertIn(gesto, ('acenar', 'espiar', 'coracao', 'surpreso')) self.assertEqual(self.v.diario[0]['tipo'], 'acao') def test_cuidados_sobem_necessidades_e_mandam_gesto(self): self.v.necessidades['social'] = 20 st = self.v.estimulo('carinho', DIA) self.assertEqual(st['necessidades']['social'], 35) self.assertEqual(self.ordens[-1], 'coracao') self.v.estimulo('ninar', DIA) self.assertTrue(self.v.dormindo()) with self.assertRaises(ValueError): self.v.estimulo('voar', DIA) def test_figurinha_com_limite(self): treino = TreinoFalso() self.v.treinamento = treino self.v.pode_gemini = lambda: True self.assertEqual(self.v.motivo_sem_emoji(DIA), '') self.assertTrue(self.v.criar_figurinha(DIA, 'feliz')) self.assertIn('à tarde', treino.pedidos[0]) self.assertEqual(self.v.motivo_sem_emoji(DIA + 60), 'descansando a criatividade') self.v.config['max_emojis_dia'] = 1 self.assertEqual(self.v.motivo_sem_emoji(DIA + 4 * 3600), 'limite do dia') self.v.pode_gemini = lambda: False self.assertEqual(self.v.motivo_sem_emoji(DIA + 4 * 3600), 'sem chave Gemini') def test_tick_manda_humor_e_respeita_bloqueio(self): self.v.proximo_ato = DIA + 999 self.v.tick(DIA + 5) self.assertTrue(any(o.startswith('humor ') for o in self.ordens)) self.ordens.clear() self.v.bloqueado = lambda: True self.v.proximo_ato = DIA self.assertIsNone(self.v.tick(DIA + 70)) self.assertEqual(self.ordens, []) def test_desligado_nao_age_e_config_persiste(self): self.v.configurar({'ligado': False, 'ritmo': 'calmo'}) self.assertIn('humor normal', self.ordens) self.v.proximo_ato = DIA self.assertIsNone(self.v.tick(DIA + 10)) outra = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False) self.assertFalse(outra.config['ligado']) self.assertEqual(outra.config['ritmo'], 'calmo') for ruim in ({'ritmo': 'turbo'}, {'max_emojis_dia': 50}, {'ligado': 1}, {'x': True}): with self.assertRaises(ValueError): self.v.configurar(ruim) class VidaHttpTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.server = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler) self.server.token = 'token-vida' self.server.vida = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False) threading.Thread(target=self.server.serve_forever, daemon=True).start() def tearDown(self): self.server.shutdown(); self.server.server_close(); self.tmp.cleanup() def pedir(self, metodo, rota, corpo=None, token='token-vida'): c = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=5) h = {'Content-Type': 'application/json'} if token: h['Authorization'] = 'Bearer ' + token c.request(metodo, rota, body=json.dumps(corpo) if corpo is not None else None, headers=h) r = c.getresponse(); dados = r.read(); c.close() return r.status, json.loads(dados) def test_rotas(self): self.assertEqual(self.pedir('GET', '/vida/status', token=None)[0], 401) self.assertEqual(self.pedir('POST', '/vida/cuidar', {'acao': 'carinho'}, token=None)[0], 401) codigo, st = self.pedir('GET', '/vida/status') self.assertEqual(codigo, 200) self.assertIn(st['humor'], ('feliz', 'tranquilo', 'carente', 'entediado', 'triste', 'cansado', 'dormindo')) codigo, st = self.pedir('POST', '/vida/cuidar', {'acao': 'brincar'}) self.assertEqual((codigo, st['diario'][0]['texto']), (200, 'Brincou')) self.assertEqual(self.pedir('POST', '/vida/cuidar', {'acao': 'voar'})[0], 400) self.assertFalse(self.pedir('POST', '/vida/config', {'ligado': False})[1]['config']['ligado']) self.assertIn('vida', self.pedir('GET', '/estado')[1]) if __name__ == '__main__': unittest.main()