"""Aprendizado com o dia a dia: gosto pelos atos, rotina da casa, sono e o fechamento do dia.""" import http.client import importlib.util import json import random import sys import tempfile import threading import time import unittest from datetime import date, timedelta from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from aprendizado import Aprendizado # noqa: E402 from vida import Vida # noqa: E402 spec = importlib.util.spec_from_file_location('kiosk_aprendizado', ROOT / 'kiosk-server.py') kiosk = importlib.util.module_from_spec(spec) spec.loader.exec_module(kiosk) def em(dia, hora, minuto=0, segundo=0): return time.mktime((2026, 10, dia, hora, minuto, segundo, 0, 0, -1)) class AprendizadoTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.caminho = Path(self.tmp.name) / 'aprendizado.json' self.a = Aprendizado(self.caminho) self.a.hoje = self.a._dia_vazio('2026-10-06') self.diario = [] self.a.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto)) def tearDown(self): self.tmp.cleanup() def fazer(self, nome, vezes, resposta=None, inicio=None): t = inicio or em(6, 15) for i in range(vezes): self.a.ato(nome, t + i * 120, perto=True) if resposta: self.a.resposta(resposta, t + i * 120 + 20) def test_gosto_sobe_com_carinho_e_desce_quando_ignorado(self): self.fazer('rir', 5, 'carinho') self.fazer('bocejar', 5, inicio=em(6, 16)) self.assertGreater(self.a.peso('rir'), 1.0) self.assertLess(self.a.peso('bocejar'), 1.0) self.assertLessEqual(self.a.peso('rir'), 2.0) self.assertGreaterEqual(self.a.peso('bocejar'), 0.5) def test_sem_opiniao_com_poucas_tentativas_ou_sozinho(self): self.fazer('rir', 2, 'carinho') self.assertEqual(self.a.peso('rir'), 1.0) for i in range(10): self.a.ato('pensar', em(6, 15) + i, perto=False) # sem plateia ninguém pode gostar self.assertEqual(self.a.peso('pensar'), 1.0) self.assertNotIn('pensar', self.a.itens) def test_resposta_fora_da_janela_nao_conta(self): for i in range(4): self.a.ato('acenar', em(6, 15) + i * 300, perto=True) self.a.resposta('carinho', em(6, 15) + i * 300 + 200) # 200 s depois: já era outra coisa self.assertEqual(self.a.itens['acenar']['s'], 0) def test_cada_ato_agrada_uma_vez(self): self.a.ato('rir', em(6, 15), perto=True) for _ in range(5): self.a.resposta('carinho', em(6, 15) + 10) self.assertEqual(self.a.itens['rir']['s'], 1.0) def test_fechar_o_dia_gera_licoes_e_esquece_um_pouco(self): self.fazer('rir', 4, 'carinho') self.fazer('bocejar', 5, inicio=em(6, 17)) t_antes = self.a.itens['rir']['t'] self.a.observar(em(7, 0, 1), perto=False) # meia-noite: fecha o dia 6 textos = [t for _, t in self.diario] self.assertTrue(any('gostam de me ver dar risada' in t for t in textos), textos) self.assertTrue(any('Tentei bocejar 5 vezes e ninguém ligou' in t for t in textos), textos) self.assertAlmostEqual(self.a.itens['rir']['t'], t_antes * 0.85, places=2) self.assertEqual(self.a.hoje['dia'], '2026-10-07') self.assertTrue(self.caminho.exists()) def test_rotina_da_casa_e_acordar_antes_de_chegarem(self): for dia in (6, 7, 8): for minuto in range(60): self.a.observar(em(dia, 17, minuto), perto=False) # 17h: casa vazia self.a.observar(em(dia, 18, minuto), perto=True) # 18h: chegou gente self.a.observar(em(9, 0, 1), perto=False) self.assertGreaterEqual(self.a.rotina[18], 0.5) self.assertLess(self.a.rotina[17], 0.3) self.assertTrue(any('costumam estar por perto das 18h às 19h' in t for _, t in self.diario), self.diario) self.assertTrue(self.a.chega_logo(em(9, 17, 50))) self.assertFalse(self.a.chega_logo(em(9, 17, 10))) # ainda falta muito self.assertFalse(self.a.chega_logo(em(9, 12, 50))) def test_acordar_a_toa_pede_mais_movimento_naquela_hora(self): for dia in (6, 7, 8, 9): for vez in range(2): t = em(dia, 22, vez * 20) self.a.acordou_pelo_sensor(t) for s in range(0, 200, 5): self.a.observar(t + s, perto=s < 10) # mexeu e sumiu: foi à toa self.a.observar(em(dia + 1, 0, 1), perto=False) self.assertGreaterEqual(self.a.extra_pir(em(10, 22, 30)), 1) self.assertEqual(self.a.extra_pir(em(10, 15)), 0) self.assertTrue(any('Acordei à toa 2 vezes por volta das 22h' in t for _, t in self.diario), self.diario) def test_acordar_com_carinho_valeu_a_pena(self): t = em(6, 22) self.a.acordou_pelo_sensor(t) self.a.observar(t + 5, perto=True) self.a.resposta('carinho', t + 30) for s in range(35, 200, 5): self.a.observar(t + s, perto=False) self.assertEqual(self.a.hoje['falsos'][22], 0) def test_desligado_nao_aprende_nem_muda_nada(self): self.a.configurar(dict(ligado=False)) self.fazer('rir', 6, 'carinho') self.assertEqual(self.a.peso('rir'), 1.0) self.assertEqual(self.a.extra_pir(em(6, 22)), 0) self.assertFalse(self.a.chega_logo(em(6, 17, 50))) def test_esquecer_e_persistencia(self): self.fazer('rir', 5, 'carinho') self.fazer('bocejar', 5, inicio=em(6, 17)) # o gosto é relativo: precisa de algo menos querido self.a.salvar() outro = Aprendizado(self.caminho) self.assertGreater(outro.peso('rir'), 1.0) outro.anotar = lambda *a, **k: None outro.esquecer() self.assertEqual(outro.peso('rir'), 1.0) self.assertEqual(Aprendizado(self.caminho).itens, {}) def test_validar(self): with self.assertRaises(ValueError): self.a.configurar(dict(ligado='sim')) with self.assertRaises(ValueError): self.a.configurar(dict(outro=True)) class VidaComAprendizadoTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.a = Aprendizado(Path(self.tmp.name) / 'aprendizado.json') self.a.hoje = self.a._dia_vazio('2026-10-06') self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3)) self.v.aprendizado = self.a self.ordens = [] self.v.enfileirar = self.ordens.append self.v.ultimo_tick = em(6, 15) def tearDown(self): self.tmp.cleanup() def test_gesto_que_agrada_sai_mais(self): for i in range(8): self.a.ato('rir', em(6, 15) + i * 100, perto=True) self.a.resposta('carinho', em(6, 15) + i * 100 + 5) self.a.ato('feliz', em(6, 15) + i * 100 + 50, perto=True) opcoes = [('rir', 1, ''), ('feliz', 1, '')] saiu = [self.v.escolher(opcoes)[0] for _ in range(400)] self.assertGreater(saiu.count('rir'), saiu.count('feliz') * 2) def test_carinho_e_atos_com_plateia_chegam_ao_aprendizado(self): self.v.alguem_perto = lambda: True self.v.celular_online = lambda: True self.v.proximo_ato = 0 self.v.necessidades.update(energia=90, social=90, diversao=90, alegria=90) ato = None for i in range(30): ato = self.v.tick(em(6, 15) + i * 200) if ato in self.a.itens: break self.assertIn(ato, self.a.itens) self.v.estimulo('carinho', em(6, 15) + i * 200 + 10) self.assertGreater(self.a.itens[ato]['s'], 0) def test_pir_pede_mais_quando_acordou_a_toa_naquela_hora(self): self.a.falsos[22] = 1.0 self.assertFalse(self.v.pir_acorda(em(6, 22))) self.assertTrue(self.v.pir_acorda(em(6, 22) + 10)) self.assertEqual(self.a.acordou_em, em(6, 22) + 10) self.assertTrue(self.v.pir_acorda(em(6, 15))) def test_acorda_do_cochilo_antes_de_chegarem(self): self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1 self.v.dormindo_proprio = True self.v.necessidades['energia'] = 70 self.assertEqual(self.v.agir(em(6, 17, 50)), 'acordar') self.assertIn('acordar', self.ordens) def test_sono_profundo_nao_e_interrompido_pela_rotina(self): self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1 self.v.dormindo_proprio = True self.v.sono_profundo = True self.v.necessidades['energia'] = 70 self.assertIsNone(self.v.agir(em(6, 17, 50))) def semana_tipica(a, semanas=2): """Segunda, sexta e fim de semana em casa (7h-23h); terça a quinta fora das 8h às 18h. Começa em 05/10 (segunda).""" inicio = date(2026, 10, 5) for i in range(semanas * 7): d = inicio + timedelta(days=i) a.hoje = a._dia_vazio(d.isoformat()) for h in range(24): for m in range(0, 60, 2): fora = d.weekday() in (1, 2, 3) and 8 <= h < 18 perto = 7 <= h < 23 and not fora and m < 40 a.minuto = None a.observar(time.mktime((d.year, d.month, d.day, h, m, 0, 0, 0, -1)), perto) fim = d + timedelta(days=1) a.observar(time.mktime((fim.year, fim.month, fim.day, 0, 0, 30, 0, 0, -1)), False) def plano_certo(): dias = [] for dia in range(7): if dia in (1, 2, 3): periodos = [dict(ini=0, fim=7, tipo='dormindo'), dict(ini=7, fim=8, tipo='casa'), dict(ini=8, fim=18, tipo='fora'), dict(ini=18, fim=23, tipo='casa'), dict(ini=23, fim=24, tipo='dormindo')] else: periodos = [dict(ini=0, fim=7, tipo='dormindo'), dict(ini=7, fim=23, tipo='casa'), dict(ini=23, fim=24, tipo='dormindo')] dias.append(dict(dia=dia, periodos=periodos)) return dict(padroes=['Percebi que de terça a quinta a casa fica vazia das 8h às 18h'], dias=dias) class SemanaTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.a = Aprendizado(Path(self.tmp.name) / 'ap.json') self.diario = [] self.a.anotar = lambda tipo, texto, agora=None: self.diario.append(texto) self.pedidos = [] self.resposta = plano_certo() def ia(provedor, prompt, schema): self.pedidos.append((provedor, prompt)) return self.resposta self.a.ia, self.a.pode_ia = ia, lambda provedor: True self.a.config['analise_ia'] = False # nos testes a análise é chamada direto def tearDown(self): self.tmp.cleanup() def test_quadro_da_semana_separa_os_dias(self): semana_tipica(self.a) self.assertEqual(len(self.a.historico), 14) self.assertLess(self.a.semana[2][10], 0.3) # quarta 10h: fora self.assertGreater(self.a.semana[0][10], 0.5) # segunda 10h: em casa self.assertGreater(self.a.semana[4][10], 0.5) # sexta 10h: em casa self.assertEqual(self.a.esperado(em(14, 10)), 'fora') # 14/10 é quarta self.assertEqual(self.a.esperado(em(12, 10)), 'casa') # 12/10 é segunda self.assertTrue(self.a.chega_logo(em(14, 17, 50))) # quarta: chegam às 18h self.assertFalse(self.a.chega_logo(em(12, 17, 50))) # segunda: já tem gente em casa self.assertTrue(self.a.casa_vazia(em(14, 10))) def test_plano_da_ia_que_bate_com_os_dados_e_usado(self): semana_tipica(self.a) self.a.analisar(em(19, 1)) self.assertIn('quarta', self.pedidos[0][1]) self.assertIn('sexta (2 dias medidos)', self.pedidos[0][1]) self.assertEqual(self.a.plano['origem'], 'ia') self.assertGreaterEqual(self.a.conferir(self.a.plano), 0.7) self.assertIsNone(self.a.sugestao) self.assertEqual(self.a.esperado(em(14, 10)), 'fora') self.assertEqual(self.a.esperado(em(14, 2)), 'dormindo') self.assertTrue(any('terça a quinta' in t for t in self.diario)) def test_plano_da_ia_que_nao_bate_vira_so_sugestao(self): semana_tipica(self.a) errado = plano_certo() for d in errado['dias']: d['periodos'] = [dict(ini=0, fim=24, tipo='fora')] # "a casa vive vazia": falso self.resposta = errado self.a.analisar(em(19, 1)) self.assertIsNone(self.a.plano) self.assertIsNotNone(self.a.sugestao) self.assertLess(self.a.conferir(self.a.sugestao), 0.7) self.assertTrue(any(t.startswith('Acho que') for t in self.diario)) self.a.usar_sugestao() self.assertEqual(self.a.plano['origem'], 'voce') def test_rotina_contada_vale_na_hora_e_a_ia_nao_atropela(self): self.a.informar('Fico fora de terça a quinta, das 8h às 18h') self.assertIn('Fico fora de terça a quinta', self.pedidos[0][1]) self.assertEqual(self.a.plano['origem'], 'voce') self.assertIsNone(self.a.conferir(self.a.plano)) # ainda sem dados para conferir semana_tipica(self.a) self.assertGreaterEqual(self.a.conferir(self.a.plano), 0.7) self.a.analisar(em(19, 1)) self.assertEqual(self.a.plano['origem'], 'voce') # o que a pessoa disse continua valendo self.assertIsNotNone(self.a.sugestao) def test_validar_plano_conserta_o_que_a_ia_erra(self): p = Aprendizado.validar_plano(dict(padroes=['ok'] * 9, dias=[ dict(dia=0, periodos=[dict(ini=8, fim=20, tipo='casa'), dict(ini=18, fim=30, tipo='fora'), dict(ini=5, fim=5, tipo='casa'), dict(ini=1, fim=3, tipo='festa')]), dict(dia=9, periodos=[dict(ini=0, fim=24, tipo='casa')]), 'lixo']), 'ia') self.assertEqual(p['dias'][0], [dict(ini=8, fim=18, tipo='casa'), dict(ini=18, fim=24, tipo='fora')]) self.assertEqual(len(p['padroes']), 5) with self.assertRaises(ValueError): Aprendizado.validar_plano(dict(padroes=[], dias=[]), 'ia') def test_analise_roda_sozinha_a_meia_noite_com_uma_semana(self): self.a.config['analise_ia'] = True semana_tipica(self.a, semanas=1) for _ in range(50): if not self.a.analise['ocupado'] and (self.a.plano or self.a.sugestao): break time.sleep(0.05) self.assertEqual(len(self.pedidos), 1) # só na 7ª meia-noite self.assertIsNotNone(self.a.plano or self.a.sugestao) def test_persistencia_da_semana_e_do_plano(self): semana_tipica(self.a, semanas=1) self.a.informar('Fico fora de terça a quinta, das 8h às 18h') self.a.salvar() outro = Aprendizado(Path(self.tmp.name) / 'ap.json') self.assertEqual(outro.semana, self.a.semana) self.assertEqual(outro.plano['dias'], self.a.plano['dias']) self.assertEqual(len(outro.historico), 7) def test_casa_vazia_faz_ele_cochilar_para_guardar_pique(self): v = Vida(Path(self.tmp.name) / 'v.json', iniciar=False, rng=random.Random(5)) v.aprendizado = self.a v.enfileirar = lambda ordem: None v.celular_online = lambda: True v.necessidades.update(energia=70, diversao=80, social=80, alegria=80) self.a.informar('Fico fora de terça a quinta, das 8h às 18h') v.visto_em = em(14, 9) atos = [v.agir(em(14, 10) + i * 120) for i in range(10)] self.assertIn('atividade dormir', atos) cochilo = next(d for d in self.a.decisoes if d['tipo'] == 'cochilo') self.assertEqual(cochilo['motivo'], 'às 10h de quarta a casa costuma ficar vazia, pelo plano que você contou') class DiaADiaTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.a = Aprendizado(Path(self.tmp.name) / 'ap.json') self.a.ia, self.a.pode_ia = (lambda provedor, prompt, schema: plano_certo()), (lambda provedor: True) self.a.config['analise_ia'] = False self.v = Vida(Path(self.tmp.name) / 'v.json', iniciar=False, rng=random.Random(2)) self.v.aprendizado = self.a self.v.enfileirar = lambda ordem: None def tearDown(self): self.tmp.cleanup() def test_cada_decisao_guarda_o_porque(self): semana_tipica(self.a) self.a.analisar(em(19, 1)) self.assertEqual(self.a.decisoes[0]['tipo'], 'ia') self.assertIn('plano adotado: confere', self.a.decisoes[0]['motivo']) # acordar antes da chegada (quarta, 17h50) self.v.dormindo_proprio = True self.v.necessidades['energia'] = 70 self.assertEqual(self.v.agir(em(14, 17, 50)), 'acordar') self.assertEqual(self.a.decisoes[0]['motivo'], 'às 18h de quarta costuma ter gente, pelo plano da IA') # sensor segurado numa hora em que acordou à toa self.a.falsos[22] = 1.0 self.v.pir_acorda(em(14, 22)) self.v.pir_acorda(em(14, 22) + 30) # a 2ª nota em menos de 10 min não repete notas = [d for d in self.a.decisoes if d['tipo'] == 'sono'] self.assertEqual(len(notas), 1) self.assertIn('pede 1 movimento a mais', notas[0]['motivo']) def test_fechar_o_dia_resume_e_confere_com_o_plano(self): self.a.informar('Fico fora de terça a quinta, das 8h às 18h') semana_tipica(self.a, semanas=1) fechou = [d for d in self.a.decisoes if d['tipo'] == 'dia'] self.assertEqual(len(fechou), 7) quarta = next(d for d in fechou if 'quarta' in d['texto']) self.assertIn('conferiu 100% com o plano', quarta['motivo']) def test_dia_a_dia_de_um_dia_passado_e_de_hoje(self): semana_tipica(self.a) self.a.analisar(em(19, 1)) d = self.a.dia_a_dia('2026-10-14') self.assertEqual(d['nome_dia'], 'quarta') self.assertFalse(d['hoje']) self.assertLess(d['medido'][10], 0.3) self.assertEqual(d['plano'][2], dict(ini=8, fim=18, tipo='fora')) self.assertEqual(d['confere'], 1.0) self.assertTrue(any(x['tipo'] == 'dia' for x in d['decisoes'])) self.assertIn('2026-10-05', d['dias']) hoje = self.a.dia_a_dia() self.assertTrue(hoje['hoje']) with self.assertRaises(ValueError): self.a.dia_a_dia('ontem') class RotasTest(unittest.TestCase): def test_status_config_e_esquecer(self): with tempfile.TemporaryDirectory() as tmp: kiosk.ARQUIVO_APARENCIA = str(Path(tmp) / 'a.json') s = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler) s.aprendizado = Aprendizado(Path(tmp) / 'ap.json') kiosk.ligar_eventos(s) threading.Thread(target=s.serve_forever, daemon=True).start() try: def pedir(metodo, rota, corpo=None): c = http.client.HTTPConnection('127.0.0.1', s.server_port, timeout=5) dados = json.dumps(corpo).encode() if corpo is not None else None c.request(metodo, rota, body=dados, headers={'Content-Type': 'application/json'} if dados else {}) r = c.getresponse() return r.status, json.loads(r.read()) cod, st = pedir('GET', '/aprendizado/status') self.assertEqual(cod, 200) self.assertEqual(len(st['rotina']), 24) cod, st = pedir('POST', '/aprendizado/config', dict(ligado=False)) self.assertEqual((cod, st['config']['ligado']), (200, False)) cod, st = pedir('POST', '/aprendizado/config', dict(ligado='x')) self.assertEqual(cod, 400) cod, st = pedir('POST', '/aprendizado/esquecer', {}) self.assertEqual((cod, st['dias']), (200, 0)) cod, est = pedir('GET', '/estado') self.assertIn('aprendizado', est) finally: s.shutdown() kiosk.EVENTOS.fechar() if __name__ == '__main__': unittest.main()