@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.
@@ -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.1.0",
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
  }