"""Ideias do dia: sinais da internet (falsos), filtro pela IA, criação das atividades, horário e "aprendi".""" import json import random import sys import tempfile import time import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from criacao import ATIVIDADES_BASE, Criacao # noqa: E402 from inspiracao import Inspiracao, descrever_tempo # noqa: E402 from vida import Vida # noqa: E402 TEMPO = dict(current=dict(temperature_2m=12.4, weather_code=63, is_day=1), daily=dict(temperature_2m_max=[15.2], temperature_2m_min=[9.8], precipitation_probability_max=[80], weather_code=[63])) FERIADOS = [dict(date='2026-10-12', name='Nossa Senhora Aparecida', type='national')] HISTORIA = dict(selected=[dict(year=1957, text='A União Soviética lança o Sputnik 1, o primeiro satélite artificial.')], events=[dict(year=1871, text='Começa o grande incêndio de Chicago.')]) ALTA = ('semana mundial do espaço' 'eleição municipal') # 8 de outubro de 2026, 10h (horário de Brasília) AGORA = time.mktime((2026, 10, 8, 10, 0, 0, 0, 0, -1)) def baixar_falso(url, timeout=10): if 'open-meteo.com/v1/forecast' in url: return json.dumps(TEMPO).encode() if 'brasilapi' in url: return json.dumps(FERIADOS).encode() if 'onthisday' in url: return json.dumps(HISTORIA).encode() if 'trends.google' in url: return ALTA.encode() if 'geocoding' in url: return json.dumps(dict(results=[dict(name='Curitiba', admin1='Paraná', latitude=-25.4284, longitude=-49.2733)])).encode() raise OSError('sem internet') class InspiracaoTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() pasta = Path(self.tmp.name) self.c = Criacao(pasta / 'c.db', pasta / 'c.json') self.prompts = [] ideias = dict(ideias=[ dict(pedido='bonequinho de astronauta pulando com estrelas em volta', motivo='Semana Mundial do Espaço', fonte='alta', aprendi='Sabia que o Sputnik foi o primeiro satélite, em 1957?'), dict(pedido='sentado lendo um livro com uma xícara, chuva lá fora', motivo='Chuva em Londrina', fonte='tempo', aprendi='A chuva enche os rios e rega as plantas!'), dict(pedido='dançando com estrelas para o Dia das Crianças', motivo='Dia das Crianças chegando', fonte='datas', aprendi='O Dia das Crianças no Brasil é em 12 de outubro.')]) def chamar(ia, prompt, schema): self.prompts.append(prompt) if 'ideias' in schema['properties']: return ideias return dict(ATIVIDADES_BASE['dancar'], nome='Atividade %d' % len(self.prompts)) self.c.chamar = chamar self.i = Inspiracao(pasta / 'i.json', self.c, iniciar=False) self.i.baixar = baixar_falso self.i.pode_gemini = lambda: True self.ordens, self.diario = [], [] self.i.enfileirar = self.ordens.append self.i.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto)) def tearDown(self): self.tmp.cleanup() def test_sinais_das_quatro_fontes(self): s = self.i.coletar(AGORA) self.assertIn('12 °C e chuva', s['tempo']) self.assertIn('80% de chance de chuva', s['tempo']) self.assertTrue(any('Nossa Senhora Aparecida' in d for d in s['datas'])) self.assertIn('em 4 dias (12/10): Dia das Crianças', s['datas']) self.assertTrue(any('Sputnik' in f for f in s['historia'])) self.assertEqual(s['alta'], ['semana mundial do espaço', 'eleição municipal']) self.assertEqual(descrever_tempo(95), 'tempestade com trovões') def test_fonte_fora_do_ar_nao_impede_as_outras_e_fontes_desligadas(self): def so_tempo(url, timeout=10): if 'forecast' in url: return json.dumps(TEMPO).encode() raise OSError('fora do ar') self.i.baixar = so_tempo self.assertEqual(list(self.i.coletar(AGORA)), ['tempo', 'datas']) # as datas fixas não dependem da internet self.i.baixar = baixar_falso self.i.configurar(dict(fontes=dict(alta=False, historia=False))) self.assertEqual(sorted(self.i.coletar(AGORA)), ['datas', 'tempo']) with self.assertRaises(ValueError): self.i.configurar(dict(fontes=dict(tempo=False, datas=False))) # sobraria nenhuma def test_cria_as_ideias_do_dia_na_hora_certa(self): cedo = AGORA - 3 * 3600 # 7h: antes das 9h não cria self.assertFalse(self.i.passo(cedo)) self.assertTrue(self.i.passo(AGORA)) self.assertEqual(len(self.i.hoje['ids']), 3) itens = self.i.itens_hoje() self.assertEqual([i['origem'] for i in itens], ['dia'] * 3) self.assertEqual(itens[0]['motivo'], 'Semana Mundial do Espaço') self.assertIn('Sputnik', itens[0]['aprendi']) # o prompt pede só temas leves e leva os sinais e o que ele já tem prompt = self.prompts[0] self.assertIn('política', prompt) self.assertIn('Londrina', prompt) self.assertIn('Dançando', prompt) self.assertIn('quinta-feira, 8 de outubro de 2026', prompt) self.assertTrue(any(t == 'ideia' for t, _ in self.diario)) self.assertFalse(self.i.passo(AGORA + 60)) # já fez as de hoje # dia seguinte: começa de novo self.assertTrue(self.i.passo(AGORA + 86400)) self.assertEqual(len(self.i.hoje['ids']), 3) def test_sem_internet_tenta_de_novo_depois_e_desiste_no_dia(self): self.i.baixar = lambda url, timeout=10: (_ for _ in ()).throw(OSError('sem rede')) chamadas = [] def ia_fora(ia, prompt, schema): chamadas.append(prompt) raise ValueError('Gemini indisponível') self.c.chamar = ia_fora self.assertTrue(self.i.passo(AGORA)) self.assertIn('Gemini indisponível', self.i.hoje['erro']) self.assertFalse(self.i.passo(AGORA + 60)) # espera 30 min self.assertTrue(self.i.passo(AGORA + 31 * 60)) self.assertTrue(self.i.passo(AGORA + 62 * 60)) self.assertFalse(self.i.passo(AGORA + 93 * 60)) # 3 tentativas por dia self.assertEqual(len(chamadas), 3) self.assertEqual(self.i.hoje['ids'], []) def test_sem_gemini_ou_desligado_nao_cria(self): self.i.pode_gemini = lambda: False self.assertFalse(self.i.passo(AGORA)) self.i.pode_gemini = lambda: True self.i.configurar(dict(ligado=False)) self.assertFalse(self.i.passo(AGORA)) def test_aprendi_hoje_conta_ate_duas_vezes(self): self.i.passo(AGORA) item = self.i.pendente() self.assertIsNotNone(item) self.i.contar(item['id']) self.i.contar(item['id']) self.assertTrue(self.ordens[0].startswith('aprendi Sabia que o Sputnik')) self.assertNotEqual(self.i.pendente()['id'], item['id']) # já contou 2x: passa para a próxima self.i.configurar(dict(aprendi=False)) self.assertIsNone(self.i.pendente()) def test_cidade_pelo_nome_e_validacao(self): st = self.i.configurar(dict(cidade='curitiba', por_dia=2, hora=14)) self.assertEqual(st['config']['cidade'], dict(nome='Curitiba (Paraná)', lat=-25.4284, lon=-49.2733)) self.assertEqual((st['config']['por_dia'], st['config']['hora']), (2, 14)) for ruim in (dict(por_dia=9), dict(hora=23), dict(ligado='sim'), dict(outra=1)): with self.assertRaises(ValueError): self.i.configurar(ruim) outra = Inspiracao(Path(self.tmp.name) / 'i.json', self.c, iniciar=False) self.assertEqual(outra.config['por_dia'], 2) class VidaComNovidadesTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3)) self.ordens = [] self.v.enfileirar = self.ordens.append self.v.celular_online = lambda: True self.v.celular_dormindo = lambda: False self.v.madrugada = lambda agora: False self.nova = dict(id=7, nome='Astronauta', emoji='🚀', motivo='Semana do Espaço', aprendi='Sabia que...', receita=dict(ATIVIDADES_BASE['dancar'], nome='Astronauta')) self.v.novidades = lambda: [self.nova] self.v.do_dia = lambda: [7] def tearDown(self): self.tmp.cleanup() def test_ideias_de_hoje_entram_na_rotina(self): escolhas = [self.v.escolher_atividade() for _ in range(200)] self.assertGreater(escolhas.count('criado:7'), 40) ato = self.v.comecar_atividade(time.time(), 'criado:7', 150) self.assertEqual(ato, 'atividade criado:7') ordem = self.ordens[-1] self.assertTrue(ordem.startswith('atividade_criada ')) self.assertEqual(json.loads(ordem[17:])['duracao'], 150) self.assertIn('Astronauta (Semana do Espaço)', self.v.diario[0]['texto']) # não repete a mesma em seguida self.v.ultima_atividade = 'criado:7' self.assertNotIn('criado:7', [self.v.escolher_atividade() for _ in range(50)]) def test_conta_o_que_aprendeu_com_gente_perto(self): contadas = [] self.v.aprendi_pendente = lambda: self.nova self.v.contar_aprendi = lambda ident, agora=None: contadas.append(ident) self.v.alguem_perto = lambda: True self.v.atividade_em = time.time() # sem folga para atividades for _ in range(30): if self.v.agir(time.time()) == 'aprendi': break self.assertEqual(contadas, [7]) if __name__ == '__main__': unittest.main()