File size: 20,069 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
"""4.6 Endurance: o Cooper (visão da tela), WSL e Linux sem interface gráfica.

Tudo numa pasta pessoal falsa; nenhum modelo de verdade (o Ollama é simulado)."""

import base64
import builtins
import io
import json
import os
import sys
import tempfile
from pathlib import Path

CASA = tempfile.mkdtemp(prefix="opentars-casa-")
os.environ["HOME"] = CASA
for var, sub in (("XDG_CONFIG_HOME", ".config"), ("XDG_DATA_HOME", ".local/share"), ("XDG_CACHE_HOME", ".cache")):
    os.environ[var] = os.path.join(CASA, sub)
os.environ["TARS_ARQUIVO_SESSOES"] = os.path.join(CASA, "sessoes.json")

from _util import carregar_tars  # noqa: E402

tars = carregar_tars()
amb, coo = tars.ambiente, tars.visao_lib
from PIL import Image  # noqa: E402

PASTA = Path(tempfile.mkdtemp(prefix="opentars-anexos-"))

# ------------------------------------------------------------
# 1. Versão
# ------------------------------------------------------------
assert tars.VERSAO == "5.2" and tars.CODINOME == "Ranger" and tars.VERSAO_EXIBIDA == "5.2 Ranger"
assert tars.REVISAO >= 2026100401
print("OK: versão 5.2 Ranger")

# ------------------------------------------------------------
# 2. Ambiente: WSL, sem interface, caminhos do Windows
# ------------------------------------------------------------
def ler_falso(conteudos):
    return lambda caminho: conteudos.get(str(caminho), "")


wsl2 = ler_falso({"/proc/sys/kernel/osrelease": "5.15.153.1-microsoft-standard-WSL2", "/proc/version": "Linux ... microsoft"})
wsl1 = ler_falso({"/proc/sys/kernel/osrelease": "4.4.0-19041-Microsoft", "/proc/version": "Linux ... Microsoft"})
puro = ler_falso({"/proc/sys/kernel/osrelease": "6.8.0-45-generic", "/proc/version": "Linux version 6.8.0 (buildd@ubuntu)"})
assert amb.versao_wsl({}, wsl2) == 2 and amb.versao_wsl({}, wsl1) == 1 and amb.versao_wsl({}, puro) is None
assert amb.versao_wsl({"WSL_DISTRO_NAME": "Ubuntu"}, puro) == 1  # a variável basta
assert amb.e_wsl({"WSL_INTEROP": "/run/WSL/1_interop"}, puro) and not amb.e_wsl({}, puro)

d = amb.detectar({"DISPLAY": ":0"}, puro)
assert d["tela"] and not d["sem_interface"] and d["wsl"] is None and d["rotulo"] == "desktop"
d = amb.detectar({}, puro)
assert d["sem_interface"] and not d["tela"] and "no graphical session" in d["rotulo"]
d = amb.detectar({"SSH_CONNECTION": "1 2 3 4"}, puro)
assert d["sem_interface"] and d["ssh"] and "SSH" in d["rotulo"]
d = amb.detectar({}, wsl2)
assert d["sem_interface"] and d["wsl"] == 2 and d["rotulo"].startswith("WSL2")
d = amb.detectar({"DISPLAY": ":0", "WAYLAND_DISPLAY": "wayland-0"}, wsl2)
assert d["wslg"] and not d["sem_interface"] and d["rotulo"] == "WSL2 with WSLg", d
d = amb.detectar({"DISPLAY": ":0", "TARS_SEM_INTERFACE": "1"}, puro)  # forçado
assert d["sem_interface"] and amb.forcado_sem_interface({"TARS_SEM_INTERFACE": "1"})

assert amb.caminho_do_windows("C:\\Users\\ana\\Meus Docs\\a b.txt") == "/mnt/c/Users/ana/Meus Docs/a b.txt"
assert amb.caminho_do_windows("d:/dados") == "/mnt/d/dados" and amb.caminho_do_windows("D:\\") == "/mnt/d"
assert amb.caminho_do_windows("\\\\wsl$\\Ubuntu\\home\\ana\\x.txt") == "/home/ana/x.txt"
assert amb.caminho_do_windows("\\\\wsl.localhost\\Ubuntu\\tmp") == "/tmp"
assert amb.caminho_do_windows("/home/ana/x") == "/home/ana/x" and not amb.parece_caminho_windows("/home/ana/x")
assert amb.parece_caminho_windows("C:\\x")

route = ("Iface\tDestination\tGateway\tFlags\nrmnet0\t00000000\t0120A8C0\t0003\nrmnet0\t0020A8C0\t00000000\t0001\n")
assert amb.gateway_padrao(route) == "192.168.32.1", amb.gateway_padrao(route)
assert amb.servidor_dns("# x\nnameserver 192.168.32.1\nnameserver 8.8.8.8\n") == "192.168.32.1"
assert amb.servidor_dns("nameserver 127.0.0.53\n") == "127.0.0.53"
cands = amb.candidatos_ollama_wsl(ler=ler_falso({"/etc/resolv.conf": "nameserver 172.20.0.1\n", "/proc/net/route": route}))
assert cands == ["http://172.20.0.1:11434", "http://192.168.32.1:11434"], cands
assert amb.candidatos_ollama_wsl(ler=ler_falso({"/etc/resolv.conf": "nameserver 127.0.0.53\n"})) == []
assert amb.abridor_de_links_do_windows(existe=lambda p: p == "explorer.exe") == ["explorer.exe"]
assert amb.abridor_de_links_do_windows(existe=lambda p: True) == ["wslview"]
assert amb.abridor_de_links_do_windows(existe=lambda p: False) is None
print("OK: ambiente: WSL1/2, WSLg, sem interface, SSH, caminhos do Windows e IP do Ollama do Windows")

# ------------------------------------------------------------
# 3. Cooper: nome, perguntas, respostas, milésimos -> pixels
# ------------------------------------------------------------
assert coo.MODELO_COOPER == "ProjectOpenTARS/cooper:1.2"       # só para reconhecer um Cooper que já esteja instalado
assert coo.COMANDO_INSTALAR == "ollama pull ProjectOpenTARS/Gargantua-2", "18/10: quem olha a tela é o Gargantua 2"
assert coo.achar_instalado(["qwen3:4b", "ProjectOpenTARS/cooper:1.2", "ProjectOpenTARS/Gargantua-2:lite", "ProjectOpenTARS/Gargantua-2:latest"]) == "ProjectOpenTARS/Gargantua-2:latest"
assert coo.achar_instalado(["ProjectOpenTARS/cooper:1.2", "ProjectOpenTARS/Gargantua-2:lite"]) == "ProjectOpenTARS/Gargantua-2:lite", "o Lite novo (com telas) vence o Cooper antigo"
assert coo.achar_instalado(["ProjectOpenTARS/Gargantua-2:lite", "ProjectOpenTARS/Gargantua-2:latest"]) == "ProjectOpenTARS/Gargantua-2:latest", "o 2B vem antes do Lite"
assert coo.achar_instalado(["ProjectOpenTARS/Gargantua:2.0-lite"]) is None, "o Lite 2.0 antigo não foi treinado com telas"
assert coo.achar_instalado(["ProjectOpenTARS/cooper:1.2", "ProjectOpenTARS/Gargantua:2.0-lite"]) == "ProjectOpenTARS/cooper:1.2"
os.environ["TARS_VISAO_NO_LITE"] = "1"
assert coo.achar_instalado(["ProjectOpenTARS/Gargantua:2.0-lite"]) == "ProjectOpenTARS/Gargantua:2.0-lite", "liberado à mão"
os.environ.pop("TARS_VISAO_NO_LITE")
assert coo.achar_instalado(["ProjectOpenTARS/Gargantua:1.2-lite", "ProjectOpenTARS/Gargantua:latest"]) is None, "o Gargantua 1.x não enxerga"
for tag in ("ProjectOpenTARS/cooper:latest", "cooper", "cooper-1.0:latest", "projectopentars/Cooper"):
    assert coo.e_cooper(tag), tag
for tag in ("gargantua:latest", "qwen3-vl:2b", "ProjectOpenTARS/Gargantua", "", None):
    assert not coo.e_cooper(tag), tag
assert coo.achar_instalado(["qwen3:4b", "cooper-1.0:latest", "ProjectOpenTARS/cooper:latest"]) == "ProjectOpenTARS/cooper:latest"
assert coo.achar_instalado(["ProjectOpenTARS/cooper:latest", "ProjectOpenTARS/cooper:1.2"]) == "ProjectOpenTARS/cooper:1.2", "o 1.2 vence o antigo"
assert coo.achar_instalado(["cooper-1.0:latest"]) == "cooper-1.0:latest" and coo.achar_instalado(["qwen3:4b"]) is None
assert coo.ligado({}) and not coo.ligado({"TARS_COOPER": "0"}) and not coo.ligado({"TARS_COOPER": "off"})

assert coo.tipo_do_alvo("the Save button") == ("button", "Save")
assert coo.tipo_do_alvo("botão Salvar") == ("button", "Salvar")
assert coo.tipo_do_alvo("Dark mode checkbox") == ("checkbox", "Dark mode")
assert coo.tipo_do_alvo("Address bar field") == ("field", "Address bar")
assert coo.tipo_do_alvo("campo Nome") == ("field", "Nome")
assert coo.tipo_do_alvo("close icon") == ("icon", "close")
assert coo.tipo_do_alvo("Settings") == ("button", "Settings")  # sem pista: botão
assert coo.tipo_do_alvo("menu Arquivo") == ("menu", "Arquivo")
assert coo.pergunta_localizar("the Save button") == (
    'Where is the button "Save" on the screen? Answer with its center as JSON {"x": 0-1000, "y": 0-1000} '
    '(thousandths of the image width and height). If it is not on the screen, answer {"found": false}.')
assert ' in the window "Notes"' in coo.pergunta_localizar("Save", janela="Notes")
assert coo.pergunta_descrever("use the Minimize icon").startswith(
    'Describe this screenshot for an assistant that must operate the computer to do: "use the Minimize icon".')

assert coo.ler_ponto('{"x": 512, "y": 340}') == ("achou", 512, 340)
assert coo.ler_ponto('```json\n{"x": 0, "y": 1000}\n```') == ("achou", 0, 1000)
assert coo.ler_ponto('Here: {"x": 12.5, "y": 99}') == ("achou", 12.5, 99)
assert coo.ler_ponto('{"found": false}') == ("ausente", None, None)
assert coo.ler_ponto("it is at (512, 340)") == ("achou", 512, 340)
for ruim in ("", None, "não sei", '{"x": 1200, "y": 5}', '{"x": "a", "y": 2}', "(5000, 40)", '{"y": 5}'):
    assert coo.ler_ponto(ruim) is None, ruim
assert coo.para_pixels(500, 500, 1920, 1080) == (960, 540)
assert coo.para_pixels(1000, 1000, 1920, 1080) == (1919, 1079) and coo.para_pixels(0, 0, 800, 600) == (0, 0)
assert coo.para_pixels(333, 666, 1280, 720) == (426, 480)
desc = 'Window "Notes". Button "Save" at (500, 500). Field "Name" at (250, 100). Odd (2000, 5).'
assert coo.descricao_em_pixels(desc, 1000, 500) == 'Window "Notes". Button "Save" at (500, 250). Field "Name" at (250, 50). Odd (2000, 5).'
assert "(960, 540)" in coo.descricao_em_pixels('Save {"x": 500, "y": 500}', 1920, 1080)
print("OK: Cooper: perguntas no formato do treino, leitura das respostas e milésimos -> pixels")

# ------------------------------------------------------------
# 4. Cooper no núcleo: clique, descrição, e nunca como IA de conversa
# ------------------------------------------------------------
COOPER = "ProjectOpenTARS/cooper:latest"
CATALOGO = {
    "qwen3:4b": {"tag": "qwen3:4b", "familia": "qwen3", "categoria": "geral", "parametros_b": 4.0, "capacidades": {"tools"},
                 "capacidades_conhecidas": True, "vram_estimado_gb": 3.0},
    COOPER: {"tag": COOPER, "familia": "qwen3vl", "categoria": "visao", "parametros_b": 2.0, "capacidades": {"vision", "completion"},
             "capacidades_conhecidas": True, "vram_estimado_gb": 2.0},
    "gemma3:4b": {"tag": "gemma3:4b", "familia": "gemma3", "categoria": "visao", "parametros_b": 4.0,
                  "capacidades": {"vision", "completion"}, "capacidades_conhecidas": True, "vram_estimado_gb": 3.0},
}
tars.catalogar_modelos = lambda forcar=False: CATALOGO
assert tars.visao_instalada() == COOPER
assert COOPER in tars.MODELOS_AJUDANTES and "cooper" in tars.MODELOS_AJUDANTES and "qwen3:4b" not in tars.MODELOS_AJUDANTES
assert "qwen2.5:0.5b" in tars.MODELOS_AJUDANTES
assert COOPER not in tars.construir_cadeia_fallback("acao"), "o Cooper não conversa"
os.environ["TARS_COOPER"] = "0"
assert tars.visao_instalada() is None
del os.environ["TARS_COOPER"]

perguntas = []
respostas = {"valor": '{"x": 250, "y": 500}'}


def chat_falso(modelo, prompt, **kw):
    perguntas.append((modelo, prompt, kw))
    return respostas["valor"]


tars._chat_unico = chat_falso
img = Image.new("RGB", (1200, 800), "white")
ponto, tag = tars.localizar_com_visao("the Save button", img)
assert ponto == (300, 400) and tag == COOPER, (ponto, tag)
modelo, prompt, kw = perguntas[-1]
assert modelo == COOPER and prompt.startswith('Where is the button "Save"') and kw["temperatura"] == 0.0 and len(kw["imagens"]) == 1
assert Image.open(io.BytesIO(base64.b64decode(kw["imagens"][0]))).size == (1200, 800)

respostas["valor"] = '{"found": false}'
assert tars.localizar_com_visao("Save", img) == ("ausente", COOPER)
respostas["valor"] = "não entendi"
assert tars.localizar_com_visao("Save", img) == (None, None)
respostas["valor"] = None
assert tars.localizar_com_visao("Save", img) == (None, None)
respostas["valor"] = '{"x": 250, "y": 500}'
assert tars.mirar_com_visao("Save", img) == (300, 400, "located 'Save' by looking at the screen")
assert tars.mirar_com_visao("Save", img, perto_de=(310, 410)) is not None
assert tars.mirar_com_visao("Save", img, perto_de=(900, 700)) is None, "longe do x/y da IA: a grade confere"

# descrição da tela: posições do Cooper já em pixels da imagem enviada
buffer = io.BytesIO()
Image.new("RGB", (1000, 500), "white").save(buffer, format="PNG")
b64 = base64.b64encode(buffer.getvalue()).decode()
respostas["valor"] = 'Window "Notes". Button "Save" at (500, 500).'
texto, quem = tars._descrever_print(b64, "salvar", "qwen3:4b")
assert quem == COOPER and "(500, 250)" in texto and "1000x500" in texto, texto
assert perguntas[-1][1].startswith("Describe this screenshot for an assistant")
respostas["valor"] = "A calm window with some buttons."  # sem posição: ainda vale, avisando
texto, quem = tars._descrever_print(b64, "salvar", "qwen3:4b")
assert quem == COOPER and "without positions" in texto
respostas["valor"] = None  # Cooper sem resposta: nada de modelo de reserva
tars._visao_estado.update(pausado_ate=0.0, quente=False, erro="")
perguntas.clear()
assert tars._descrever_print(b64, "salvar", "qwen3:4b") == (None, None) and {p[0] for p in perguntas} == {COOPER}, "só o Cooper é consultado"
print("OK: Cooper acha e descreve; sem resposta, só o motivo (nenhum outro modelo de visão); nunca conversa")

# ------------------------------------------------------------
# 5. Sem interface gráfica
# ------------------------------------------------------------
tars.SEM_INTERFACE = True
tars.AMBIENTE.update(sem_interface=True, tela=False)
nomes = {f["function"]["name"] for f in tars.ferramentas_do_pedido("acao", "abra o firefox e clique em entrar")}
assert nomes and not (nomes & tars.FERRAMENTAS_SO_COM_TELA), nomes & tars.FERRAMENTAS_SO_COM_TELA
nomes_tec = {f["function"]["name"] for f in tars.ferramentas_do_pedido("tecnico", "quanta memória livre eu tenho")}
assert "execute_terminal" in nomes_tec and "take_screenshot" not in nomes_tec
for ferramenta in ("take_screenshot", "click_mouse", "open_application", "list_windows", "type_text", "hotkey"):
    r = tars.executar_ferramenta(ferramenta, {"app": "calculadora", "text": "x", "keys": ["ctrl", "c"]})
    assert not r["sucesso"] and "no graphical session" in r["mensagem"] and ferramenta in r["mensagem"], (ferramenta, r)
r = tars.executar_ferramenta("execute_terminal", {"command": "echo ola"})
assert r["sucesso"] and "ola" in json.dumps(r), r
r = tars.executar_ferramenta("list_files", {"path": str(PASTA)})
assert r["sucesso"], r
r = tars.abrir_site("https://example.com")
assert not r["sucesso"] and "https://example.com" in r["mensagem"] and "link" in r["mensagem"], r
r = tars.pesquisar_web("gatos")
assert not r["sucesso"] and "google.com/search?q=gatos" in r["mensagem"], r
# no WSL sem tela, o link abre no navegador do Windows
chamadas = []
tars.EM_WSL = True
tars.ambiente.abridor_de_links_do_windows = lambda existe=None: ["wslview"]
tars.subprocess.Popen = lambda cmd, **kw: chamadas.append(cmd)
r = tars.abrir_site("https://example.com")
assert r["sucesso"] and chamadas == [["wslview", "https://example.com"]], (r, chamadas)
tars.EM_WSL = False
avisos = tars.avisos_de_ambiente()
assert any("tery" not in a and ("Sem sessão gráfica" in a or "Sem sess" in a) for a in avisos), avisos
assert not any("mouse" in a.lower() for a in avisos), avisos
tars._contexto_sistema_cache.clear()
assert "NO graphical session" in tars.contexto_sistema()
tars._contexto_sistema_cache.clear()
tars.EM_WSL = True
assert "Running inside WSL" in tars.contexto_sistema()
tars.EM_WSL = False
tars._contexto_sistema_cache.clear()
# com tela, tudo volta
tars.SEM_INTERFACE = False
tars.AMBIENTE.update(sem_interface=False, tela=True)
nomes = {f["function"]["name"] for f in tars.ferramentas_do_pedido("busca", "pesquise gatos")}
assert "take_screenshot" in nomes or "click_element" in nomes, nomes
assert "NO graphical session" not in tars.contexto_sistema()
print("OK: sem interface: ferramentas de tela somem e, se chamadas, explicam; links vão pro navegador do Windows no WSL")

# ------------------------------------------------------------
# 8. Conversa no terminal
# ------------------------------------------------------------
pedidos = []
tars.processar_mensagem = lambda texto: pedidos.append(texto) or "ok"
tars.iniciar_conversa = lambda: 0
tars.encerrar_todas_ias = lambda *a, **k: None
tars.avisos_de_ambiente = lambda: ["aviso de teste"]
entradas = []
builtins_input = builtins.input
builtins.input = lambda prompt="": entradas.pop(0) if entradas else (_ for _ in ()).throw(EOFError())
saida = io.StringIO()
real_stdout = sys.stdout
try:
    sys.stdout = saida
    entradas[:] = ["/ajuda", "resuma isto", "/sair", "isto não deveria ser lido"]
    assert tars.modo_chat_terminal() == 0
finally:
    sys.stdout = real_stdout
    builtins.input = builtins_input
assert pedidos == ["resuma isto"], pedidos
assert "aviso de teste" in saida.getvalue()
assert entradas == ["isto não deveria ser lido"], "o /sair encerra o laço"
entradas[:] = ["pergunta pelo pipe"]
pedidos.clear()
builtins.input = lambda prompt="": entradas.pop(0) if entradas else (_ for _ in ()).throw(EOFError())
try:
    sys.stdout = io.StringIO()
    assert tars.modo_chat_terminal() == 0
finally:
    sys.stdout = real_stdout
    builtins.input = builtins_input
assert pedidos == ["pergunta pelo pipe"]
print("OK: opentars --chat: pedido, /ajuda, /sair e entrada por pipe")

# --cooper (descontinuado em 18/10: só avisa e diz como instalar o Gargantua 2; não baixa nada)
rodou = []
tars.subprocess.run = lambda cmd, **kw: rodou.append(cmd) or type("R", (), {"returncode": 0})()
saida = io.StringIO()
try:
    sys.stdout = saida
    assert tars.comando_cooper() == 0
finally:
    sys.stdout = real_stdout
assert not rodou and "ollama pull ProjectOpenTARS/Gargantua-2" in saida.getvalue() and "descontinuado" in saida.getvalue(), saida.getvalue()
print("OK: opentars --cooper avisa que o Cooper foi descontinuado e aponta o Gargantua 2")

# ------------------------------------------------------------
# 9. Textos nos dois idiomas
# ------------------------------------------------------------
base = Path(tars.__file__).parent / "idiomas"
en, pt = json.loads((base / "en.json").read_text()), json.loads((base / "pt_BR.json").read_text())
assert set(en) == set(pt), sorted(set(en) ^ set(pt))
for chave in ("chat.ajuda", "visao.nao_instalado", "cooper.descontinuado", "diag.wsl_sem_ollama",
              "term.onde_conversar_sem_interface", "aviso.modo_sem_interface"):
    assert en[chave] and pt[chave], chave
assert "--chat" in en["term.ajuda"] and "--cooper" not in pt["term.ajuda"]
print("OK: textos novos nos dois idiomas")

print("\nTUDO OK.")

# 4.5.1: Cooper exclusivo (nada de Bonsai/LM Studio de reserva)
os.environ.pop("TARS_COOPER", None)
tars.visao_instalada = lambda: COOPER
_envio = []
tars._chat_unico = lambda modelo, prompt, **kw: (_envio.append(modelo), "A window with a Save button")[1]
img2 = Image.new("RGB", (1200, 800), "white")
buf = io.BytesIO(); img2.save(buf, format="PNG")
import base64 as _b64
b64 = _b64.b64encode(buf.getvalue()).decode()
texto, tag = tars.descrever_com_visao(b64, "salvar")
assert tag == COOPER and "Save button" in texto and set(_envio) == {COOPER}, "descrição sem posições também vale (exclusivo)"
print("OK: Cooper exclusivo: nenhum outro modelo de visão é chamado")

# 4.5.1: Cooper lento/fora do ar não trava os passos seguintes
_chamadas = []


def _lento(modelo, prompt, **kw):
    _chamadas.append(kw["timeout"])
    tars._ultimo_erro["texto"] = "Read timed out"
    return None


tars._chat_unico = _lento
tars.visao_instalada = lambda: COOPER
tars._visao_estado.update(pausado_ate=0.0, quente=False, erro="")
assert tars.perguntar_visao(COOPER, "q", imagem_b64=b64) is None
assert _chamadas == [tars.TIMEOUT_VISAO_FRIO], "prazo estourado: sem segunda tentativa"
assert tars.visao_disponivel() is None, "depois de estourar o prazo o Cooper descansa"
assert tars.motivo_da_falha_da_visao() == "Read timed out"
assert tars.descrever_com_visao(b64, "x") == (None, None) and len(_chamadas) == 1, "em pausa: nem pergunta"
tars._visao_estado.update(pausado_ate=0.0)
_chamadas.clear()


def _erro_rapido(modelo, prompt, **kw):
    _chamadas.append(kw["timeout"])
    tars._ultimo_erro["texto"] = "HTTP 500 model failed to load"
    return None


tars._chat_unico = _erro_rapido
assert tars.perguntar_visao(COOPER, "q", imagem_b64=b64) is None and len(_chamadas) == 2, "erro rápido: tenta de novo uma vez"
assert tars.visao_disponivel() == COOPER, "erro rápido não põe em pausa"
tars._visao_estado.update(pausado_ate=0.0, quente=False, erro="")
print("OK: Cooper lento ganha pausa, erro rápido tenta duas vezes, e o motivo fica registrado")