@fefsbenson/jarvis 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +446 -1
- package/bin/teste-preservacao.mjs +65 -0
- package/bin/teste-update-backup.py +89 -0
- package/bin/teste-update-cliente.py +133 -0
- package/bin/teste-update-entrega.py +88 -0
- package/bin/teste-update-hostil.py +147 -0
- package/bin/teste-update-preserva.py +112 -0
- package/bin/testes.sh +29 -0
- package/package.json +5 -2
- package/bin/cli.js.bak-antes-windows +0 -297
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""BATERIA 4 — o cliente que usou o JARVIS por meses, e atualiza.
|
|
3
|
+
|
|
4
|
+
Não são casos isolados: é UMA instalação com 60+ artefatos espalhados por todas
|
|
5
|
+
as áreas onde um dono real trabalha. Depois do update, TUDO tem que estar lá.
|
|
6
|
+
E roda a cadeia inteira de versões (1.1.20 -> 1.1.23 -> 1.1.24) para provar que
|
|
7
|
+
o update repetido não acumula estrago.
|
|
8
|
+
"""
|
|
9
|
+
import hashlib, json, shutil, subprocess, sys, tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PAC = Path.home()/"jarvis-dist"
|
|
13
|
+
CLI = Path.home()/"jarvis-npx/bin/cli.js"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 60+ artefatos: tudo que um dono acumula em meses de uso
|
|
20
|
+
TRABALHO = {}
|
|
21
|
+
for i in range(8):
|
|
22
|
+
TRABALHO[f"workspace/businesses/acme/copy/pagina-{i}.md"] = f"copy de venda {i}"
|
|
23
|
+
TRABALHO[f"workspace/ops/sprints/sprint-{i}.md"] = f"sprint {i}"
|
|
24
|
+
for i in range(6):
|
|
25
|
+
TRABALHO[f"knowledge/business/insights/reuniao-{i}.md"] = f"ata {i} CONFIDENCIAL"
|
|
26
|
+
TRABALHO[f"knowledge/personal/cognitive/diario-{i}.md"] = f"reflexao {i}"
|
|
27
|
+
TRABALHO[f"outputs/entrega-cliente-{i}.md"] = f"entrega paga {i}"
|
|
28
|
+
for i in range(5):
|
|
29
|
+
TRABALHO[f"knowledge/external/inbox/CURSO-{i}/aula.txt"] = f"material comprado {i}"
|
|
30
|
+
TRABALHO[f"research/analise-{i}.md"] = f"pesquisa {i}"
|
|
31
|
+
TRABALHO[f"logs/batches/b-{i}.jsonl"] = '{"batch":%d}' % i
|
|
32
|
+
for ag in ("decisor", "conclave/sintetizador"):
|
|
33
|
+
TRABALHO[f"agents/system/{ag}/MEMORY.md"] = f"MEMORIA ACUMULADA de {ag}"
|
|
34
|
+
TRABALHO[f"agents/system/{ag}/SOUL.md"] = f"voz calibrada de {ag}"
|
|
35
|
+
TRABALHO.update({
|
|
36
|
+
".env": "OPENAI_API_KEY=sk-real\nVOYAGE_API_KEY=vk-real",
|
|
37
|
+
"jarvis.config.yaml": "voz: onyx\ntratamento: chefe",
|
|
38
|
+
".claude/settings.local.json": '{"hooks":{"Stop":[]}}',
|
|
39
|
+
".claude/jarvis/JARVIS-MEMORY.md": "memoria relacional de 6 meses",
|
|
40
|
+
".claude/jarvis/PENDING.md": "- pendencia do dono",
|
|
41
|
+
".claude/sessions/SESSION-antiga.md": "sessao de marco",
|
|
42
|
+
".claude/mission-control/MISSION-STATE.json": '{"fase":4}',
|
|
43
|
+
".claude/trash/apagado.md": "mandei pra lixeira",
|
|
44
|
+
".claude/rules/minha-regra-propria.md": "regra que EU escrevi",
|
|
45
|
+
".claude/hooks/meu_hook_proprio.py": "# hook que EU escrevi",
|
|
46
|
+
".claude/skills/minha-skill/SKILL.md": "skill que EU criei",
|
|
47
|
+
"artifacts/audit/relatorio.json": '{"ok":true}',
|
|
48
|
+
"processing/speakers.json": '{"s":1}',
|
|
49
|
+
".data/rag_expert/vectors.json": '{"dim":1024}',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
def montar(versao, com_manifesto=True):
|
|
53
|
+
B = Path(tempfile.gettempdir())/"cliente-real"
|
|
54
|
+
if B.exists(): shutil.rmtree(B, ignore_errors=True)
|
|
55
|
+
inst = B/"inst"
|
|
56
|
+
for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
|
|
57
|
+
"agents/system", "core/intelligence/agents",
|
|
58
|
+
"knowledge/external/dna"):
|
|
59
|
+
if (PAC/a).is_dir():
|
|
60
|
+
shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
|
|
61
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
62
|
+
for f in ("VERSION", ".claude/settings.json", "install.py"):
|
|
63
|
+
(inst/f).parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
shutil.copy2(PAC/f, inst/f)
|
|
65
|
+
(inst/"VERSION").write_text(versao + "\n")
|
|
66
|
+
if com_manifesto:
|
|
67
|
+
arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
|
|
68
|
+
for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
|
|
69
|
+
mp = inst/".claude/jarvis/instalacao-original.json"
|
|
70
|
+
mp.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
mp.write_text(json.dumps({"versao": versao, "arquivos": arq}, indent=2))
|
|
72
|
+
for rel, txt in TRABALHO.items():
|
|
73
|
+
p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
p.write_text(txt, encoding="utf-8")
|
|
75
|
+
return inst
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def conferir(inst, rot):
|
|
79
|
+
perdidos = []
|
|
80
|
+
for rel, esperado in TRABALHO.items():
|
|
81
|
+
p = inst/rel
|
|
82
|
+
if not p.is_file():
|
|
83
|
+
perdidos.append(f"{rel}(SUMIU)"); continue
|
|
84
|
+
atual = p.read_text(encoding="utf-8", errors="replace")
|
|
85
|
+
if esperado in atual:
|
|
86
|
+
continue
|
|
87
|
+
if rel.endswith(".json"):
|
|
88
|
+
try:
|
|
89
|
+
if all(json.loads(atual).get(k) == v
|
|
90
|
+
for k, v in json.loads(esperado).items()):
|
|
91
|
+
continue
|
|
92
|
+
except Exception: pass
|
|
93
|
+
if (inst/(rel+".antigo")).is_file() and \
|
|
94
|
+
esperado in (inst/(rel+".antigo")).read_text(encoding="utf-8", errors="replace"):
|
|
95
|
+
continue
|
|
96
|
+
perdidos.append(f"{rel}(PERDIDO)")
|
|
97
|
+
ok = not perdidos
|
|
98
|
+
print(f" {'PASS' if ok else 'FALHA'} [{rot}] {len(TRABALHO)} artefatos do dono"
|
|
99
|
+
+ ("" if ok else f" -> {len(perdidos)} perdidos: {perdidos[:6]}"))
|
|
100
|
+
return ok
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
falhas = 0
|
|
104
|
+
print(f" simulando um cliente com {len(TRABALHO)} artefatos acumulados\n")
|
|
105
|
+
|
|
106
|
+
# A — com manifesto, uma atualização
|
|
107
|
+
inst = montar("1.1.23")
|
|
108
|
+
subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
109
|
+
capture_output=True, text=True, timeout=600)
|
|
110
|
+
falhas += not conferir(inst, "1.1.23 -> 1.1.24, com manifesto")
|
|
111
|
+
|
|
112
|
+
# B — sem manifesto (instalação antiga, o caso do Fábio)
|
|
113
|
+
inst = montar("1.1.20", com_manifesto=False)
|
|
114
|
+
subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
115
|
+
capture_output=True, text=True, timeout=600)
|
|
116
|
+
falhas += not conferir(inst, "1.1.20 -> 1.1.24, SEM manifesto")
|
|
117
|
+
|
|
118
|
+
# C — três updates seguidos: o estrago não pode acumular
|
|
119
|
+
inst = montar("1.1.23")
|
|
120
|
+
for n in range(3):
|
|
121
|
+
subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
122
|
+
capture_output=True, text=True, timeout=600)
|
|
123
|
+
falhas += not conferir(inst, "três updates seguidos")
|
|
124
|
+
|
|
125
|
+
# D — o caminho Python, sobre a mesma instalação
|
|
126
|
+
inst = montar("1.1.23")
|
|
127
|
+
subprocess.run([sys.executable, str(PAC/".claude/scripts/atualizar.py"),
|
|
128
|
+
"--novo", str(PAC), "--destino", str(inst), "--sim"],
|
|
129
|
+
capture_output=True, text=True, timeout=600)
|
|
130
|
+
falhas += not conferir(inst, "caminho Python (atualizar.py)")
|
|
131
|
+
|
|
132
|
+
print(f"\n BATERIA 4: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
|
|
133
|
+
sys.exit(1 if falhas else 0)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""BATERIA 5 — o update ENTREGA a correção?
|
|
3
|
+
|
|
4
|
+
Preservar tudo é trivial se você não atualiza nada. Esta bateria prova o outro
|
|
5
|
+
lado: o que é do sistema e o dono não tocou TEM que receber a versão nova.
|
|
6
|
+
Um update que só preserva é um update quebrado que parece seguro.
|
|
7
|
+
"""
|
|
8
|
+
import hashlib, json, shutil, subprocess, sys, tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
PAC = Path.home()/"jarvis-dist"
|
|
12
|
+
CLI = Path.home()/"jarvis-npx/bin/cli.js"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def montar(nome, envelhecer: dict, com_manifesto=True):
|
|
19
|
+
"""Instala e depois 'envelhece' arquivos do sistema (simula versão antiga)."""
|
|
20
|
+
B = Path(tempfile.gettempdir())/f"entrega-{nome}"
|
|
21
|
+
if B.exists(): shutil.rmtree(B, ignore_errors=True)
|
|
22
|
+
inst = B/"inst"
|
|
23
|
+
for a in (".claude/hooks", ".claude/rules", ".claude/scripts",
|
|
24
|
+
".claude/skills/jarvis", "agents/system", "core/intelligence/agents",
|
|
25
|
+
"knowledge/external/dna"):
|
|
26
|
+
if (PAC/a).is_dir():
|
|
27
|
+
shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
|
|
28
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
29
|
+
for f in ("VERSION", ".claude/settings.json", "install.py"):
|
|
30
|
+
(inst/f).parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
shutil.copy2(PAC/f, inst/f)
|
|
32
|
+
(inst/"VERSION").write_text("1.1.23\n")
|
|
33
|
+
# ENVELHECE: escreve a "versão antiga" nesses arquivos do sistema
|
|
34
|
+
for rel, txt in envelhecer.items():
|
|
35
|
+
p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
p.write_text(txt, encoding="utf-8")
|
|
37
|
+
# manifesto reflete o estado ENVELHECIDO (o dono não tocou nesses arquivos)
|
|
38
|
+
if com_manifesto:
|
|
39
|
+
arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
|
|
40
|
+
for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
|
|
41
|
+
mp = inst/".claude/jarvis/instalacao-original.json"
|
|
42
|
+
mp.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
|
|
44
|
+
return inst
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Arquivos do SISTEMA "na versão antiga" — o dono nunca os tocou.
|
|
48
|
+
# Cada um representa uma família: hook, rule, script, skill, agente, DNA.
|
|
49
|
+
ANTIGOS = {
|
|
50
|
+
".claude/hooks/hook_output.py": "# VERSAO ANTIGA COM BUG\n",
|
|
51
|
+
".claude/rules/goal-mindset.md": "# regra desatualizada\n",
|
|
52
|
+
".claude/scripts/jarvis_doctor.py": "# doctor antigo\n",
|
|
53
|
+
".claude/skills/jarvis/SKILL.md": "# skill antiga\n",
|
|
54
|
+
"agents/system/decisor/AGENT.md": "# agente desatualizado\n",
|
|
55
|
+
"core/intelligence/agents/activation_generator.py": "# gerador antigo\n",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
falhas = 0
|
|
59
|
+
for com_man in (True, False):
|
|
60
|
+
rot = "com manifesto" if com_man else "SEM manifesto"
|
|
61
|
+
print(f"\n ── {rot} ──")
|
|
62
|
+
inst = montar("m" if com_man else "s", ANTIGOS, com_man)
|
|
63
|
+
r = subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
64
|
+
capture_output=True, text=True, timeout=600)
|
|
65
|
+
for rel, antigo in ANTIGOS.items():
|
|
66
|
+
p, orig = inst/rel, PAC/rel
|
|
67
|
+
if not orig.is_file():
|
|
68
|
+
print(f" --- {rel} não existe no pacote (pulando)"); continue
|
|
69
|
+
atual = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else ""
|
|
70
|
+
recebeu = h(p) == h(orig) if p.is_file() else False
|
|
71
|
+
ainda_antigo = antigo.strip() in atual
|
|
72
|
+
ok = recebeu and not ainda_antigo
|
|
73
|
+
falhas += not ok
|
|
74
|
+
print(f" {'PASS' if ok else 'FALHA'} {rel}"
|
|
75
|
+
+ ("" if ok else f" -> recebeu_versao_nova={recebeu} ainda_antigo={ainda_antigo}"))
|
|
76
|
+
# e a versão tem que ter subido
|
|
77
|
+
v = (inst/"VERSION").read_text().strip()
|
|
78
|
+
ok = v == "1.1.24"; falhas += not ok
|
|
79
|
+
print(f" {'PASS' if ok else 'FALHA'} VERSION -> {v}")
|
|
80
|
+
# e os arquivos NOVOS da 1.1.24 têm que ter chegado
|
|
81
|
+
for rel in (".claude/skills/jarvis-update/SKILL.md",
|
|
82
|
+
".claude/rules/design-por-publico.md",
|
|
83
|
+
".claude/scripts/atualizar.py"):
|
|
84
|
+
ok = (inst/rel).is_file(); falhas += not ok
|
|
85
|
+
print(f" {'PASS' if ok else 'FALHA'} novo da 1.1.24: {rel}")
|
|
86
|
+
|
|
87
|
+
print(f"\n BATERIA 5: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
|
|
88
|
+
sys.exit(1 if falhas else 0)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""BATERIA 3 — casos hostis: o que quebra um update no mundo real.
|
|
3
|
+
|
|
4
|
+
Cada caso é uma situação que existe na máquina de gente de verdade e que um
|
|
5
|
+
update ingênuo trata mal: rodar duas vezes, arquivo somente-leitura, nome com
|
|
6
|
+
acento e espaço, link simbólico, arquivo enorme, pasta que não é JARVIS,
|
|
7
|
+
pacote inválido, e interrupção no meio.
|
|
8
|
+
"""
|
|
9
|
+
import hashlib, json, os, shutil, stat, subprocess, sys, tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PAC = Path.home()/"jarvis-dist"
|
|
13
|
+
CLI = Path.home()/"jarvis-npx/bin/cli.js"
|
|
14
|
+
UPD = PAC/".claude/scripts/atualizar.py"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def base(nome, com_manifesto=True):
|
|
21
|
+
B = Path(tempfile.gettempdir())/f"host-{nome}"
|
|
22
|
+
if B.exists():
|
|
23
|
+
shutil.rmtree(B, ignore_errors=True)
|
|
24
|
+
inst = B/"inst"
|
|
25
|
+
for a in (".claude/hooks", ".claude/rules"):
|
|
26
|
+
shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
|
|
27
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
28
|
+
(inst/"VERSION").write_text("1.1.23\n")
|
|
29
|
+
if com_manifesto:
|
|
30
|
+
arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
|
|
31
|
+
for f in inst.rglob("*") if f.is_file()}
|
|
32
|
+
mp = inst/".claude/jarvis/instalacao-original.json"
|
|
33
|
+
mp.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
|
|
35
|
+
return inst
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def upd(inst, *extra, pacote=None):
|
|
39
|
+
return subprocess.run(
|
|
40
|
+
["node", str(CLI), "update", str(inst), "--de", str(pacote or PAC), "--sim", *extra],
|
|
41
|
+
capture_output=True, text=True, timeout=600)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
resultados = []
|
|
45
|
+
def caso(nome, ok, detalhe=""):
|
|
46
|
+
resultados.append((nome, ok, detalhe))
|
|
47
|
+
print(f" {'PASS' if ok else 'FALHA'} {nome}" + (f" -> {detalhe}" if not ok else ""))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# 1 — IDEMPOTÊNCIA: rodar duas vezes seguidas não pode mudar nada na segunda
|
|
51
|
+
inst = base("idem")
|
|
52
|
+
(inst/"workspace").mkdir(exist_ok=True); (inst/"workspace/x.md").write_text("do dono")
|
|
53
|
+
upd(inst)
|
|
54
|
+
estado1 = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*")
|
|
55
|
+
if f.is_file() and ".claude/jarvis" not in str(f) and "sessions" not in str(f)}
|
|
56
|
+
r2 = upd(inst)
|
|
57
|
+
estado2 = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*")
|
|
58
|
+
if f.is_file() and ".claude/jarvis" not in str(f) and "sessions" not in str(f)}
|
|
59
|
+
dif = [k for k in estado2 if estado1.get(k) != estado2[k]]
|
|
60
|
+
caso("idempotente (2a rodada não muda nada)", not dif, f"{len(dif)} mudaram: {dif[:3]}")
|
|
61
|
+
|
|
62
|
+
# 2 — a 2a rodada deve dizer "já está na versão mais recente"
|
|
63
|
+
caso("2a rodada reconhece que já está atualizado",
|
|
64
|
+
"mais recente" in r2.stdout or "Nada a mudar" in r2.stdout,
|
|
65
|
+
r2.stdout[-200:])
|
|
66
|
+
|
|
67
|
+
# 3 — NOME COM ACENTO E ESPAÇO (Windows/pt-BR é cheio disso)
|
|
68
|
+
inst = base("acento")
|
|
69
|
+
d = inst/"workspace/Relatórios do Cliente"
|
|
70
|
+
d.mkdir(parents=True); (d/"análise final.md").write_text("conteúdo com acento")
|
|
71
|
+
r = upd(inst)
|
|
72
|
+
caso("nome com acento e espaço preservado",
|
|
73
|
+
r.returncode == 0 and (d/"análise final.md").read_text() == "conteúdo com acento",
|
|
74
|
+
f"rc={r.returncode}")
|
|
75
|
+
|
|
76
|
+
# 4 — ARQUIVO SOMENTE-LEITURA do dono
|
|
77
|
+
inst = base("readonly")
|
|
78
|
+
f = inst/"workspace/protegido.md"
|
|
79
|
+
f.parent.mkdir(parents=True, exist_ok=True); f.write_text("read-only do dono")
|
|
80
|
+
os.chmod(f, stat.S_IRUSR)
|
|
81
|
+
r = upd(inst)
|
|
82
|
+
ok = r.returncode == 0 and f.read_text() == "read-only do dono"
|
|
83
|
+
os.chmod(f, stat.S_IRUSR | stat.S_IWUSR)
|
|
84
|
+
caso("arquivo somente-leitura do dono intacto", ok, f"rc={r.returncode}")
|
|
85
|
+
|
|
86
|
+
# 5 — LINK SIMBÓLICO (quem move o workspace para outro disco faz isso)
|
|
87
|
+
inst = base("symlink")
|
|
88
|
+
alvo = inst.parent/"fora"; alvo.mkdir()
|
|
89
|
+
(alvo/"real.md").write_text("mora fora do JARVIS")
|
|
90
|
+
(inst/"workspace").mkdir(exist_ok=True)
|
|
91
|
+
os.symlink(alvo/"real.md", inst/"workspace/link.md")
|
|
92
|
+
r = upd(inst)
|
|
93
|
+
caso("link simbólico não é seguido nem quebrado",
|
|
94
|
+
r.returncode == 0 and (alvo/"real.md").read_text() == "mora fora do JARVIS",
|
|
95
|
+
f"rc={r.returncode}")
|
|
96
|
+
|
|
97
|
+
# 6 — PASTA QUE NÃO É JARVIS: tem que recusar, não estragar
|
|
98
|
+
vazio = Path(tempfile.gettempdir())/"host-naojarvis"
|
|
99
|
+
if vazio.exists(): shutil.rmtree(vazio)
|
|
100
|
+
vazio.mkdir(); (vazio/"documento-importante.docx").write_text("nada a ver")
|
|
101
|
+
r = upd(vazio)
|
|
102
|
+
caso("recusa pasta que não é JARVIS",
|
|
103
|
+
r.returncode != 0 and (vazio/"documento-importante.docx").is_file(),
|
|
104
|
+
f"rc={r.returncode}")
|
|
105
|
+
|
|
106
|
+
# 7 — PACOTE INVÁLIDO: recusa sem tocar na instalação
|
|
107
|
+
inst = base("pacinval")
|
|
108
|
+
(inst/"workspace").mkdir(exist_ok=True); (inst/"workspace/x.md").write_text("do dono")
|
|
109
|
+
lixo = Path(tempfile.gettempdir())/"host-pacote-lixo"
|
|
110
|
+
if lixo.exists(): shutil.rmtree(lixo)
|
|
111
|
+
lixo.mkdir(); (lixo/"README").write_text("não sou um pacote")
|
|
112
|
+
antes = h(inst/"workspace/x.md")
|
|
113
|
+
r = upd(inst, pacote=lixo)
|
|
114
|
+
caso("recusa pacote inválido sem tocar na instalação",
|
|
115
|
+
r.returncode != 0 and h(inst/"workspace/x.md") == antes,
|
|
116
|
+
f"rc={r.returncode}")
|
|
117
|
+
|
|
118
|
+
# 8 — --seco NÃO ESCREVE NADA (a promessa mais importante)
|
|
119
|
+
inst = base("seco")
|
|
120
|
+
antes = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*") if f.is_file()}
|
|
121
|
+
r = upd(inst, "--seco")
|
|
122
|
+
depois = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*") if f.is_file()}
|
|
123
|
+
caso("--seco não escreve absolutamente nada",
|
|
124
|
+
r.returncode == 0 and antes == depois,
|
|
125
|
+
f"rc={r.returncode} · {len(set(depois)-set(antes))} novos")
|
|
126
|
+
|
|
127
|
+
# 9 — o PACOTE DE ORIGEM nunca é modificado nem apagado
|
|
128
|
+
inst = base("origem")
|
|
129
|
+
pac_v, pac_n = (PAC/"VERSION").read_text(), sum(1 for _ in (PAC/".claude/scripts").iterdir())
|
|
130
|
+
upd(inst)
|
|
131
|
+
caso("pacote de origem intacto após o update",
|
|
132
|
+
(PAC/"VERSION").read_text() == pac_v
|
|
133
|
+
and sum(1 for _ in (PAC/".claude/scripts").iterdir()) == pac_n)
|
|
134
|
+
|
|
135
|
+
# 10 — arquivo GRANDE do dono não é corrompido
|
|
136
|
+
inst = base("grande")
|
|
137
|
+
g = inst/"workspace/base.csv"
|
|
138
|
+
g.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
g.write_text("col\n" + "\n".join(str(i) for i in range(200_000)))
|
|
140
|
+
antes = h(g)
|
|
141
|
+
r = upd(inst)
|
|
142
|
+
caso("arquivo grande do dono intacto", r.returncode == 0 and h(g) == antes)
|
|
143
|
+
|
|
144
|
+
falhas = sum(1 for _, ok, _ in resultados if not ok)
|
|
145
|
+
print(f"\n BATERIA 3: {len(resultados)-falhas}/{len(resultados)}"
|
|
146
|
+
+ (" TODOS PASSARAM" if not falhas else f" — {falhas} FALHA(S)"))
|
|
147
|
+
sys.exit(1 if falhas else 0)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""BATERIA 2 — o comando REAL (`npx jarvis update`), adversarial.
|
|
3
|
+
|
|
4
|
+
Roda `node cli.js update <inst> --de <pacote>` e confere o DISCO depois.
|
|
5
|
+
É o caminho que o cliente usa; testar a função extraída seria aproximação.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib, json, shutil, subprocess, sys, tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
PAC = Path.home()/"jarvis-dist"
|
|
11
|
+
CLI = Path.home()/"jarvis-npx/bin/cli.js"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def montar(nome, trabalho, com_manifesto=True):
|
|
18
|
+
B = Path(tempfile.gettempdir())/f"real-{nome}"
|
|
19
|
+
if B.exists(): shutil.rmtree(B)
|
|
20
|
+
inst = B/"inst"
|
|
21
|
+
for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
|
|
22
|
+
"agents/system", "core/intelligence/agents"):
|
|
23
|
+
if (PAC/a).is_dir():
|
|
24
|
+
shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
|
|
25
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
26
|
+
for f in ("VERSION", ".claude/settings.json", "install.py"):
|
|
27
|
+
(inst/f).parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
shutil.copy2(PAC/f, inst/f)
|
|
29
|
+
(inst/"VERSION").write_text("1.1.23\n")
|
|
30
|
+
if com_manifesto:
|
|
31
|
+
arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
|
|
32
|
+
for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
|
|
33
|
+
mp = inst/".claude/jarvis/instalacao-original.json"
|
|
34
|
+
mp.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
|
|
36
|
+
for rel, txt in trabalho.items():
|
|
37
|
+
p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
p.write_text(txt, encoding="utf-8")
|
|
39
|
+
return inst
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
CASOS = [
|
|
43
|
+
("workspace", {"workspace/businesses/acme/brand.md": "marca do cliente",
|
|
44
|
+
"workspace/gestao/cockpit.yaml": "receita: 1000000"}),
|
|
45
|
+
("memoria", {"agents/system/decisor/MEMORY.md": "SEIS MESES DE USO",
|
|
46
|
+
"agents/system/decisor/SOUL.md": "voz calibrada"}),
|
|
47
|
+
("credenciais", {".env": "OPENAI_API_KEY=sk-real", "jarvis.config.yaml": "voz: onyx"}),
|
|
48
|
+
("conhecimento",{"knowledge/business/i.md": "ata confidencial",
|
|
49
|
+
"knowledge/personal/d.md": "diario",
|
|
50
|
+
"knowledge/external/inbox/CURSO/a.txt": "material comprado"}),
|
|
51
|
+
("producao", {"outputs/entrega.md": "trabalho pago",
|
|
52
|
+
"research/analise.md": "pesquisa", "logs/s.jsonl": "{}"}),
|
|
53
|
+
("estado", {".claude/jarvis/STATE.json": '{"fase":9}',
|
|
54
|
+
".claude/sessions/L.md": "sessao",
|
|
55
|
+
".claude/settings.local.json": '{"hooks":{}}'}),
|
|
56
|
+
("hook-editado",{".claude/hooks/hook_output.py": "# EU EDITEI ESTE\n"}),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
falhas = 0
|
|
60
|
+
for com_man in (True, False):
|
|
61
|
+
rot = "COM manifesto" if com_man else "SEM manifesto (instalação antiga)"
|
|
62
|
+
print(f"\n ── {rot} ──")
|
|
63
|
+
for nome, trabalho in CASOS:
|
|
64
|
+
inst = montar(nome, trabalho, com_man)
|
|
65
|
+
antes = {r: h(inst/r) for r in trabalho}
|
|
66
|
+
pac_antes = (PAC/"VERSION").read_text(), len(list((PAC/".claude/scripts").iterdir()))
|
|
67
|
+
r = subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
68
|
+
capture_output=True, text=True, timeout=600)
|
|
69
|
+
perdidos = []
|
|
70
|
+
for rel, conteudo_dono in trabalho.items():
|
|
71
|
+
p = inst/rel
|
|
72
|
+
if not p.is_file():
|
|
73
|
+
perdidos.append(f"{rel}(SUMIU)")
|
|
74
|
+
continue
|
|
75
|
+
if h(p) == antes[rel]:
|
|
76
|
+
continue # intacto byte-a-byte
|
|
77
|
+
# Mudou. Duas razões legítimas, e nenhuma perde o dado do dono:
|
|
78
|
+
# 1. o doctor roda ao final e atualiza arquivos de ESTADO
|
|
79
|
+
# (STATE.json ganha session/mis) — o dado do dono continua lá;
|
|
80
|
+
# 2. sem manifesto, o motor atualiza COM backup .antigo — e a
|
|
81
|
+
# versão do dono está recuperável ao lado.
|
|
82
|
+
# Perda de verdade = o conteúdo dele sumiu E não há backup.
|
|
83
|
+
atual = p.read_text(encoding="utf-8", errors="replace")
|
|
84
|
+
marca = conteudo_dono.strip().splitlines()[0] if conteudo_dono.strip() else ""
|
|
85
|
+
if marca and marca in atual:
|
|
86
|
+
continue # o dado do dono sobreviveu
|
|
87
|
+
# JSON: o doctor reformata e ACRESCENTA chaves de estado. Comparar
|
|
88
|
+
# texto acusaria perda onde não houve — o que importa é se cada
|
|
89
|
+
# chave do dono continua lá, com o mesmo valor.
|
|
90
|
+
if rel.endswith(".json"):
|
|
91
|
+
try:
|
|
92
|
+
meu, agora = json.loads(conteudo_dono), json.loads(atual)
|
|
93
|
+
if all(agora.get(k) == v for k, v in meu.items()):
|
|
94
|
+
continue
|
|
95
|
+
except (json.JSONDecodeError, AttributeError):
|
|
96
|
+
pass
|
|
97
|
+
if (inst/(rel + ".antigo")).is_file():
|
|
98
|
+
bkp = (inst/(rel + ".antigo")).read_text(encoding="utf-8", errors="replace")
|
|
99
|
+
if marca and marca in bkp:
|
|
100
|
+
continue # preservado no backup
|
|
101
|
+
perdidos.append(f"{rel}(PERDIDO — sem backup)")
|
|
102
|
+
# o pacote de origem NÃO pode ter sido tocado
|
|
103
|
+
pac_depois = (PAC/"VERSION").read_text(), len(list((PAC/".claude/scripts").iterdir()))
|
|
104
|
+
if pac_antes != pac_depois:
|
|
105
|
+
perdidos.append(f"PACOTE DE ORIGEM MUDOU ({pac_antes}->{pac_depois})")
|
|
106
|
+
ok = not perdidos and r.returncode == 0
|
|
107
|
+
falhas += not ok
|
|
108
|
+
print(f" {'PASS' if ok else 'FALHA'} [{nome}] rc={r.returncode}"
|
|
109
|
+
+ ("" if ok else f" -> {perdidos} {r.stderr[:150]}"))
|
|
110
|
+
|
|
111
|
+
print(f"\n BATERIA 2: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
|
|
112
|
+
sys.exit(1 if falhas else 0)
|
package/bin/testes.sh
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Toda a bateria do update, de uma vez.
|
|
3
|
+
#
|
|
4
|
+
# Um update que apaga trabalho de cliente é o pior defeito que este pacote pode
|
|
5
|
+
# ter — pior que não atualizar, porque não tem volta e ninguém percebe na hora.
|
|
6
|
+
# Por isso a bateria é grande e roda contra o comando REAL, não contra funções
|
|
7
|
+
# extraídas: aproximação do comando mede outra coisa.
|
|
8
|
+
#
|
|
9
|
+
# Precisa de um pacote em ~/jarvis-dist para comparar.
|
|
10
|
+
set -u
|
|
11
|
+
cd "$(dirname "$0")"
|
|
12
|
+
falhas=0
|
|
13
|
+
echo
|
|
14
|
+
for t in teste-preservacao.mjs teste-update-preserva.py teste-update-hostil.py \
|
|
15
|
+
teste-update-entrega.py teste-update-backup.py teste-update-cliente.py; do
|
|
16
|
+
echo "── $t ──"
|
|
17
|
+
case "$t" in
|
|
18
|
+
*.mjs) node "$t" | tail -2 ;;
|
|
19
|
+
*.py) python3 "$t" | tail -2 ;;
|
|
20
|
+
esac
|
|
21
|
+
[ "${PIPESTATUS[0]}" -eq 0 ] || { falhas=$((falhas+1)); echo " ^^ FALHOU"; }
|
|
22
|
+
echo
|
|
23
|
+
done
|
|
24
|
+
if [ "$falhas" -eq 0 ]; then
|
|
25
|
+
echo " TODAS AS BATERIAS PASSARAM"
|
|
26
|
+
else
|
|
27
|
+
echo " $falhas BATERIA(S) COM FALHA — não publique"
|
|
28
|
+
fi
|
|
29
|
+
exit "$falhas"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fefsbenson/jarvis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "JARVIS — sistema operacional de conhecimento e agentes para Claude Code (Windows e Linux)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"jarvis": "bin/cli.js"
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"agents",
|
|
27
27
|
"knowledge-management",
|
|
28
28
|
"pragma"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "bash bin/testes.sh"
|
|
32
|
+
}
|
|
30
33
|
}
|