@pathbit/agentdevkit 0.0.17 → 0.0.19
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/.pbai4devs/commands/sincronizar/COMMAND.md +6 -4
- package/.pbai4devs/scripts/lib/gitignore.mjs +100 -43
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +12 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +70 -31
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +155 -17
- package/.pbai4devs/scripts/sincronizar-agentes/tests/PRESERVATION.mjs +306 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +11 -3
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +23 -6
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +14 -7
- package/.pbai4devs/templates/gitignore/shared.ignore +1 -0
- package/CHANGELOG.md +11 -0
- package/README.en.md +1 -1
- package/README.es.md +2 -2
- package/docs/guides/cli.md +48 -15
- package/docs/guides/installation.md +49 -23
- package/docs/guides/troubleshooting.md +3 -2
- package/docs/product/design.md +6 -6
- package/docs/specs/README.md +2 -0
- package/docs/specs/client-ignore/SPEC.md +21 -0
- package/docs/specs/safe-synchronization/SPEC.md +21 -0
- package/package.json +1 -1
- package/src/i18n.mjs +10 -0
- package/src/install.mjs +227 -28
|
@@ -42,7 +42,7 @@ test('sem Git preserva ignore e informa pendência; após git init protege outpu
|
|
|
42
42
|
git(root, ['add', '--all']);
|
|
43
43
|
const tracked = git(root, ['ls-files', '-z']).split('\0').filter(Boolean);
|
|
44
44
|
for (const path of outputs) assert.equal(tracked.includes(path), false, path);
|
|
45
|
-
for (const path of ['.github/workflows/ci.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.vscode/settings.json', '.github/skills/human/SKILL.md']) assert.
|
|
45
|
+
for (const path of ['.github/workflows/ci.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.vscode/settings.json', '.github/skills/human/SKILL.md']) assert.equal(tracked.includes(path), false, path);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test('prefixos e projetos distintos preservam blocos locais independentes', t => {
|
|
@@ -58,13 +58,49 @@ test('prefixos e projetos distintos preservam blocos locais independentes', t =>
|
|
|
58
58
|
assert.match(content, /first\\ space/); assert.match(content, /second/);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test('
|
|
61
|
+
test('projeto aninhado sob ancestral ignorado falha antes de alterar índices ou excludes', t => {
|
|
62
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
63
|
+
put(root, '.gitignore', '/.github/\n');
|
|
64
|
+
const child = join(root, '.github/project');
|
|
65
|
+
put(child, '.gitignore', '!/.claude/\n');
|
|
66
|
+
put(child, '.claude/custom.md', 'instruções próprias\n');
|
|
67
|
+
const exclude = readFileSync(join(root, '.git/info/exclude'));
|
|
68
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
69
|
+
const plan = planLocalIgnore(child, { outputs: ['.claude/custom.md'] });
|
|
70
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes('Ancestral Git ignorado: .github/')));
|
|
71
|
+
assert.throws(() => applyLocalIgnore(plan), /Ancestral Git ignorado/);
|
|
72
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
73
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
74
|
+
assert.equal(readFileSync(join(child, '.claude/custom.md'), 'utf8'), 'instruções próprias\n');
|
|
75
|
+
put(root, '.gitignore', '/.github/\n!/.github/\n');
|
|
76
|
+
const opened = planLocalIgnore(child, { outputs: ['.claude/custom.md'] });
|
|
77
|
+
applyLocalIgnore(opened);
|
|
78
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(child, { outputs: ['.claude/custom.md'] })).ok, true);
|
|
79
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.github/project/.claude/custom.md']).status, 1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('opt-in aninhado respeita também excludesFile configurado pelo usuário', t => {
|
|
83
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
84
|
+
put(root, 'global-ignore', '/.private/\n');
|
|
85
|
+
git(root, ['config', '--local', 'core.excludesFile', join(root, 'global-ignore')]);
|
|
86
|
+
const child = join(root, '.private/project');
|
|
87
|
+
put(child, '.gitignore', '!/.claude/\n');
|
|
88
|
+
put(child, '.claude/custom.md', 'instruções próprias\n');
|
|
89
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
90
|
+
const plan = planLocalIgnore(child);
|
|
91
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes('Ancestral Git ignorado: .private/')));
|
|
92
|
+
assert.throws(() => applyLocalIgnore(plan), /Ancestral Git ignorado/);
|
|
93
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
94
|
+
assert.equal(readFileSync(join(child, '.claude/custom.md'), 'utf8'), 'instruções próprias\n');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('check identifica negação humana sem alterar nem diagnosticar conteúdo rastreado legítimo', t => {
|
|
62
98
|
const root = fixture(t), outputs = ['generated.md']; git(root, ['init', '-q']);
|
|
63
99
|
put(root, 'generated.md', 'gerado'); git(root, ['add', 'generated.md']);
|
|
64
100
|
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
65
101
|
put(root, '.gitignore', '!/generated.md\n');
|
|
66
102
|
const result = checkLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
67
|
-
assert.equal(result.ok, false); assert.deepEqual(result.missing, outputs); assert.deepEqual(result.trackedOutputs,
|
|
103
|
+
assert.equal(result.ok, false); assert.deepEqual(result.missing, outputs); assert.deepEqual(result.trackedOutputs, []);
|
|
68
104
|
assert.equal(git(root, ['ls-files', 'generated.md']).trim(), 'generated.md');
|
|
69
105
|
});
|
|
70
106
|
|
|
@@ -89,7 +125,7 @@ test('exceções de templates preservam o ignore raiz e protegem dados privados'
|
|
|
89
125
|
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.CACHE/private.json']).includes('.cursor/.CACHE/private.json'));
|
|
90
126
|
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.cache/private.json']).includes('.cursor/.cache/private.json'));
|
|
91
127
|
const visible = spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.cursor/human.md']);
|
|
92
|
-
assert.equal(visible.status,
|
|
128
|
+
assert.equal(visible.status, 0);
|
|
93
129
|
applyLocalIgnore(planLocalIgnore(root));
|
|
94
130
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), updated);
|
|
95
131
|
put(root, '.gitignore', updated + '# edição humana posterior\n');
|
|
@@ -134,12 +170,12 @@ test('negação humana posterior é informada sem reescrever a raiz', t => {
|
|
|
134
170
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
135
171
|
put(root, '.gitignore', '# humano\n'); applyLocalIgnore(planLocalIgnore(root));
|
|
136
172
|
const path = join(root, '.gitignore');
|
|
137
|
-
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/.env.production\n');
|
|
173
|
+
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/\n!/.cursor/.env.production\n');
|
|
138
174
|
put(root, '.cursor/.env.production', 'privado');
|
|
139
175
|
const plan = planLocalIgnore(root);
|
|
140
176
|
assert.ok(checkLocalIgnore(plan).missing.includes('.cursor/.env.production'));
|
|
141
177
|
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
142
|
-
assert.equal(readFileSync(path, 'utf8'), '# humano\n!/.cursor/.env.production\n');
|
|
178
|
+
assert.equal(readFileSync(path, 'utf8'), '# humano\n!/.cursor/\n!/.cursor/.env.production\n');
|
|
143
179
|
});
|
|
144
180
|
|
|
145
181
|
test('planejamento sem Git não modifica ignore nem edição humana posterior', t => {
|
|
@@ -193,11 +229,13 @@ test('desindexação não descarta edição humana staged com workingtree restau
|
|
|
193
229
|
assert.equal(readFileSync(join(root, 'generated.md'), 'utf8'), 'gerado\n');
|
|
194
230
|
});
|
|
195
231
|
|
|
196
|
-
test('configurações humanas
|
|
232
|
+
test('configurações humanas podem ser versionadas após exceção explícita', t => {
|
|
197
233
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
198
234
|
applyLocalIgnore(planLocalIgnore(root));
|
|
199
235
|
put(root, '.codex/config.toml', 'local'); put(root, '.claude/settings.json', 'local');
|
|
200
236
|
put(root, '.github/settings.json', 'fonte'); put(root, '.vscode/settings.json', 'fonte');
|
|
237
|
+
put(root, '.gitignore', '!/.codex/\n!/.claude/\n!/.github/\n!/.vscode/\n');
|
|
238
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
201
239
|
git(root, ['add', '--all']);
|
|
202
240
|
const tracked = git(root, ['ls-files', '-z']).split('\0');
|
|
203
241
|
assert.equal(tracked.includes('.codex/config.toml'), true);
|
|
@@ -205,6 +243,53 @@ test('configurações humanas dos clientes continuam versionáveis', t => {
|
|
|
205
243
|
assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
|
|
206
244
|
});
|
|
207
245
|
|
|
246
|
+
test('exceções de raiz usam precedência real e mantêm credenciais ignoradas', t => {
|
|
247
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
248
|
+
put(root, '.gitignore', '!/.claude/\n!/.github/\n');
|
|
249
|
+
put(root, '.claude/skills/team/SKILL.md', 'skill humana\n');
|
|
250
|
+
put(root, '.claude/auth.json', 'segredo');
|
|
251
|
+
put(root, '.github/workflows/ci.yml', 'workflow humano\n');
|
|
252
|
+
put(root, '.vscode/settings.json', 'configuração humana\n');
|
|
253
|
+
const original = readFileSync(join(root, '.gitignore'));
|
|
254
|
+
const plan = planLocalIgnore(root, { outputs: ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml'] });
|
|
255
|
+
assert.ok(plan.optedRoots.includes('.claude'));
|
|
256
|
+
assert.ok(plan.optedRoots.includes('.github'));
|
|
257
|
+
applyLocalIgnore(plan);
|
|
258
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
259
|
+
for (const path of ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml']) {
|
|
260
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', path]).status, 1, path);
|
|
261
|
+
}
|
|
262
|
+
for (const path of ['.claude/auth.json', '.vscode/settings.json']) {
|
|
263
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', path]).status, 0, path);
|
|
264
|
+
}
|
|
265
|
+
assert.deepEqual(readFileSync(join(root, '.gitignore')), original);
|
|
266
|
+
put(root, '.gitignore', '!/.claude/\n/.claude/\n');
|
|
267
|
+
const overridden = planLocalIgnore(root);
|
|
268
|
+
assert.equal(overridden.optedRoots.includes('.claude'), false);
|
|
269
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.claude/skills/team/SKILL.md']).status, 0);
|
|
270
|
+
put(root, '.gitignore', '!/.claude/\n!/.claude/**\n');
|
|
271
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('derivados e entradas legítimas já rastreados permanecem no índice com ou sem opt-in', t => {
|
|
275
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
276
|
+
put(root, '.claude/skills/team/SKILL.md', 'skill rastreada\n');
|
|
277
|
+
put(root, '.github/workflows/ci.yml', 'workflow rastreado\n');
|
|
278
|
+
put(root, 'AGENTS.md', 'instruções rastreadas\n');
|
|
279
|
+
git(root, ['add', '--all']);
|
|
280
|
+
const before = git(root, ['ls-files', '--stage', '-z']);
|
|
281
|
+
const outputs = ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml', 'AGENTS.md'];
|
|
282
|
+
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
283
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok, true);
|
|
284
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), before);
|
|
285
|
+
put(root, '.gitignore', '!/.claude/\n');
|
|
286
|
+
const opted = planLocalIgnore(root, { outputs });
|
|
287
|
+
assert.ok(opted.optedRoots.includes('.claude'));
|
|
288
|
+
applyLocalIgnore(opted);
|
|
289
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok, true);
|
|
290
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), before);
|
|
291
|
+
});
|
|
292
|
+
|
|
208
293
|
test('artefatos vêm da política compartilhada e rastreados são informados', t => {
|
|
209
294
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
210
295
|
put(root, '.agent-tests/human.txt', 'evidência humana'); git(root, ['add', '-f', '.agent-tests/human.txt']);
|
|
@@ -271,12 +356,13 @@ test('probes reconhecem exceções humanas para nomes reais de Playwright', asyn
|
|
|
271
356
|
});
|
|
272
357
|
});
|
|
273
358
|
|
|
274
|
-
test('milhares de caminhos usam stdin
|
|
359
|
+
test('milhares de caminhos usam stdin sem ampliar metadata de área já ignorada', t => {
|
|
275
360
|
const root = fixture(t); git(root, ['init', '-q']); put(root, '.gitignore', '/ignored/\n');
|
|
276
361
|
const before = readFileSync(join(root, '.git/info/exclude'), 'utf8');
|
|
277
362
|
const target = join(root, 'ignored'); mkdirSync(target);
|
|
278
|
-
const outputs = Array.from({ length: 1500 }, (_, index) =>
|
|
363
|
+
const outputs = Array.from({ length: 1500 }, (_, index) => `generated-${index}-${'x'.repeat(45)}.md`);
|
|
279
364
|
const plan = planLocalIgnore(target, { outputs });
|
|
365
|
+
assert.ok(plan.paths.length >= 1500);
|
|
280
366
|
assert.deepEqual(plan.edits, []); assert.equal(checkLocalIgnore(plan).ok, true);
|
|
281
367
|
applyLocalIgnore(plan); assert.equal(readFileSync(join(root, '.git/info/exclude'), 'utf8'), before);
|
|
282
368
|
assert.deepEqual(planUntrack(target, Object.fromEntries(outputs.map(path => [path, 'a'.repeat(64)]))).paths, []);
|
|
@@ -341,16 +427,17 @@ test('cliente não selecionado com symlink próprio não é atravessado pelo pro
|
|
|
341
427
|
assert.deepEqual(readdirSync(outside), before);
|
|
342
428
|
});
|
|
343
429
|
|
|
344
|
-
test('preflight
|
|
430
|
+
test('preflight valida hash do bloco substituído sem impor exclusão sobre cliente optado', t => {
|
|
345
431
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
346
432
|
const block = '# BEGIN AGENT IGNORE\r\n!/generated.md\r\n# END AGENT IGNORE';
|
|
433
|
+
put(root, '.gitignore', '!/.github/\n');
|
|
347
434
|
put(root, '.github/.gitignore', `# Humano\r\n${block}\r\n!/human.md\r\n`);
|
|
348
435
|
const plan = planLocalIgnore(root, { outputs: ['.github/generated.md', '.github/human.md'] });
|
|
349
436
|
const replacement = { path: join(root, '.github/.gitignore'), firstLine: 2, lastLine: 4, sha256: createHash('sha256').update(block).digest('hex') };
|
|
350
|
-
assert.deepEqual(checkLocalIgnore(plan).missing, [
|
|
351
|
-
assert.deepEqual(checkLocalIgnore(plan, { replacedBlocks: [replacement] }).missing, [
|
|
437
|
+
assert.deepEqual(checkLocalIgnore(plan).missing, []);
|
|
438
|
+
assert.deepEqual(checkLocalIgnore(plan, { replacedBlocks: [replacement] }).missing, []);
|
|
352
439
|
assert.throws(() => checkLocalIgnore(plan, { replacedBlocks: [{ ...replacement, sha256: '0'.repeat(64) }] }), /Bloco de ignore mudou/);
|
|
353
|
-
assert.
|
|
440
|
+
assert.doesNotThrow(() => applyLocalIgnore(plan));
|
|
354
441
|
assert.equal(readFileSync(join(root, '.github/.gitignore'), 'utf8'), `# Humano\r\n${block}\r\n!/human.md\r\n`);
|
|
355
442
|
});
|
|
356
443
|
|
|
@@ -490,14 +577,14 @@ test('dependências já ignoradas não causam conflito, mas ignores visíveis s
|
|
|
490
577
|
assert.ok(checkLocalIgnore(planLocalIgnore(root)).missing.some(value => value.includes('!.playwright-*/')));
|
|
491
578
|
});
|
|
492
579
|
|
|
493
|
-
test('bloco
|
|
580
|
+
test('bloco de outra worktree não bloqueia a política local de clientes', t => {
|
|
494
581
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
495
582
|
const old = '# BEGIN AGENTDEVKIT LOCAL 0123456789abcdef\n/.claude/\n# END AGENTDEVKIT LOCAL 0123456789abcdef\n';
|
|
496
583
|
put(root, '.git/info/exclude', old);
|
|
497
584
|
const plan = planLocalIgnore(root);
|
|
498
|
-
assert.
|
|
499
|
-
assert.
|
|
500
|
-
assert.
|
|
585
|
+
assert.equal(checkLocalIgnore(plan).missing.length, 0);
|
|
586
|
+
assert.doesNotThrow(() => applyLocalIgnore(plan));
|
|
587
|
+
assert.ok(readFileSync(join(root, '.git/info/exclude'), 'utf8').startsWith(old));
|
|
501
588
|
});
|
|
502
589
|
|
|
503
590
|
test('artefatos já versionados são informados sem alterar o índice', t => {
|
|
@@ -555,6 +642,57 @@ test('escrita de exclusões preserva permissões e detecta alteração concorren
|
|
|
555
642
|
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
556
643
|
});
|
|
557
644
|
|
|
645
|
+
test('alteração no exclude durante escrita temporária é preservada antes da promoção', t => {
|
|
646
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
647
|
+
const file = join(root, '.git/info/exclude'), before = readFileSync(file, 'utf8');
|
|
648
|
+
const plan = planLocalIgnore(root), index = git(root, ['ls-files', '--stage', '-z']);
|
|
649
|
+
const write = fs.writeFileSync; let injected = false;
|
|
650
|
+
try {
|
|
651
|
+
fs.writeFileSync = (path, content, options) => {
|
|
652
|
+
const value = write(path, content, options);
|
|
653
|
+
if (!injected && String(path).startsWith(`${file}.agentdevkit-`)) {
|
|
654
|
+
injected = true;
|
|
655
|
+
write(file, `${before}\n# regra humana concorrente\n`);
|
|
656
|
+
}
|
|
657
|
+
return value;
|
|
658
|
+
};
|
|
659
|
+
syncBuiltinESMExports();
|
|
660
|
+
assert.throws(() => applyLocalIgnore(plan), /Exclusao alterada antes de publicar/);
|
|
661
|
+
} finally { fs.writeFileSync = write; syncBuiltinESMExports(); }
|
|
662
|
+
assert.equal(injected, true);
|
|
663
|
+
assert.equal(readFileSync(file, 'utf8'), `${before}\n# regra humana concorrente\n`);
|
|
664
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('alteração no exclude durante rollback não é substituída', t => {
|
|
668
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
669
|
+
const file = join(root, '.git/info/exclude'), plan = planLocalIgnore(root);
|
|
670
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
671
|
+
const write = fs.writeFileSync, remove = fs.rmSync;
|
|
672
|
+
let failed = false, injected = false;
|
|
673
|
+
try {
|
|
674
|
+
fs.rmSync = (path, options) => {
|
|
675
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) {
|
|
676
|
+
failed = true; throw new Error('falha após promoção');
|
|
677
|
+
}
|
|
678
|
+
return remove(path, options);
|
|
679
|
+
};
|
|
680
|
+
fs.writeFileSync = (path, content, options) => {
|
|
681
|
+
const value = write(path, content, options);
|
|
682
|
+
if (failed && !injected && String(path).startsWith(`${file}.agentdevkit-`)) {
|
|
683
|
+
injected = true;
|
|
684
|
+
write(file, `${readFileSync(file, 'utf8')}\n# regra humana no rollback\n`);
|
|
685
|
+
}
|
|
686
|
+
return value;
|
|
687
|
+
};
|
|
688
|
+
syncBuiltinESMExports();
|
|
689
|
+
assert.throws(() => applyLocalIgnore(plan), /rollback incompleto.*conteudo preservado/s);
|
|
690
|
+
} finally { fs.rmSync = remove; fs.writeFileSync = write; syncBuiltinESMExports(); }
|
|
691
|
+
assert.equal(injected, true);
|
|
692
|
+
assert.match(readFileSync(file, 'utf8'), /# regra humana no rollback/);
|
|
693
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
694
|
+
});
|
|
695
|
+
|
|
558
696
|
test('ignore raiz mantém modo e bytes antes e depois de git init', { skip: process.platform === 'win32' }, t => {
|
|
559
697
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
560
698
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { synchronize } from '../SCRIPT.mjs';
|
|
9
|
+
import { indexContentMatches } from '../../lib/gitignore.mjs';
|
|
10
|
+
|
|
11
|
+
const repository = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
12
|
+
const original = '.pbai4devs/skills/projeto/criar-skill/SKILL.md';
|
|
13
|
+
const pointer = '.claude/skills/criar-skill/SKILL.md';
|
|
14
|
+
const file = (root, relative) => join(root, relative);
|
|
15
|
+
const put = (root, relative, content) => { mkdirSync(dirname(file(root, relative)), { recursive: true }); writeFileSync(file(root, relative), content); };
|
|
16
|
+
function git(root, ...args) {
|
|
17
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
18
|
+
assert.equal(result.status, 0, `${args.join(' ')}: ${result.stderr}`);
|
|
19
|
+
return result.stdout;
|
|
20
|
+
}
|
|
21
|
+
function fixture(t, withGit = true) {
|
|
22
|
+
mkdirSync(join(repository, '.agent-tests'), { recursive: true });
|
|
23
|
+
const root = mkdtempSync(join(withGit ? join(repository, '.agent-tests') : tmpdir(), 'sync-preservation-'));
|
|
24
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
25
|
+
if (withGit) git(root, 'init', '--quiet');
|
|
26
|
+
populate(root);
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
function populate(root) {
|
|
30
|
+
put(root, '.gitignore', '# Human ignore\nnode_modules/\n');
|
|
31
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
|
|
32
|
+
put(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
|
|
33
|
+
put(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Shared\n/tools/*/.local/\n');
|
|
34
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
35
|
+
put(root, original, '---\nname: criar-skill\ndescription: Criar recurso.\n---\n\n# Criar skill\n');
|
|
36
|
+
assert.equal(run(root).exitCode, 0);
|
|
37
|
+
assert.ok(existsSync(file(root, pointer)));
|
|
38
|
+
}
|
|
39
|
+
function run(root, options = {}) {
|
|
40
|
+
return synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
|
|
41
|
+
}
|
|
42
|
+
function withoutGit(t, callback) {
|
|
43
|
+
const emptyPath = mkdtempSync(join(tmpdir(), 'sync-empty-path-'));
|
|
44
|
+
t.after(() => rmSync(emptyPath, { recursive: true, force: true }));
|
|
45
|
+
const keys = Object.keys(process.env).filter(key => key.toLowerCase() === 'path');
|
|
46
|
+
const previous = new Map(keys.map(key => [key, process.env[key]]));
|
|
47
|
+
if (!keys.length) keys.push('PATH');
|
|
48
|
+
try {
|
|
49
|
+
for (const key of keys) process.env[key] = emptyPath;
|
|
50
|
+
return callback();
|
|
51
|
+
} finally {
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
if (previous.has(key)) process.env[key] = previous.get(key);
|
|
54
|
+
else delete process.env[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [withGit, force] of [[true, false], [true, true], [false, false], [false, true]]) {
|
|
60
|
+
test(`remoção obsoleta preserva pasta vazia humana (git=${withGit}, force=${force})`, t => {
|
|
61
|
+
const root = fixture(t, withGit), human = '.claude/skills/criar-skill/Equipe-Vazia';
|
|
62
|
+
mkdirSync(file(root, human));
|
|
63
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
64
|
+
const index = withGit ? git(root, 'ls-files', '--stage', '-z') : null;
|
|
65
|
+
const ignore = readFileSync(file(root, '.gitignore'));
|
|
66
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
67
|
+
const check = run(root, { check: true, force });
|
|
68
|
+
assert.notEqual(check.exitCode, 0);
|
|
69
|
+
assert.ok(existsSync(file(root, pointer)));
|
|
70
|
+
assert.ok(existsSync(file(root, human)));
|
|
71
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
72
|
+
run(root, { force });
|
|
73
|
+
assert.equal(existsSync(file(root, pointer)), false, 'apontamento obsoleto comprovado é removido');
|
|
74
|
+
assert.ok(existsSync(file(root, human)), 'pasta vazia própria permanece');
|
|
75
|
+
assert.deepEqual(readFileSync(file(root, '.gitignore')), ignore);
|
|
76
|
+
if (withGit) assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const force of [false, true]) {
|
|
81
|
+
test(`índice com conteúdo humano divergente bloqueia remoção (force=${force})`, t => {
|
|
82
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
83
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nConteúdo humano staged.\n')]));
|
|
84
|
+
git(root, 'add', '-f', '--', pointer);
|
|
85
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
86
|
+
put(root, pointer, generated);
|
|
87
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
88
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
89
|
+
const check = run(root, { check: true, force });
|
|
90
|
+
assert.notEqual(check.exitCode, 0);
|
|
91
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
92
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
93
|
+
assert.throws(() => run(root, { force }), /conflitos|indice|índice/i);
|
|
94
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
95
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const force of [false, true]) {
|
|
100
|
+
test(`índice divergente impede atualização de apontamento ativo (force=${force})`, t => {
|
|
101
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
102
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
103
|
+
git(root, 'add', '-f', '--', pointer);
|
|
104
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
105
|
+
put(root, pointer, generated);
|
|
106
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
107
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
108
|
+
assert.notEqual(run(root, { check: true, force }).exitCode, 0);
|
|
109
|
+
assert.throws(() => run(root, { force }), /indice/i);
|
|
110
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
111
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
112
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const flag of ['--assume-unchanged', '--skip-worktree']) {
|
|
117
|
+
test(`índice divergente continua protegido com ${flag}`, t => {
|
|
118
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
119
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
120
|
+
git(root, 'add', '-f', '--', pointer);
|
|
121
|
+
put(root, pointer, generated);
|
|
122
|
+
git(root, 'update-index', flag, '--', pointer);
|
|
123
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
124
|
+
const flags = git(root, 'ls-files', '-v', '--', pointer);
|
|
125
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
126
|
+
assert.notEqual(run(root, { check: true, force: true }).exitCode, 0);
|
|
127
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
128
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
129
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
130
|
+
assert.equal(git(root, 'ls-files', '-v', '--', pointer), flags);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test('índice com alias de caixa bloqueia alteração conservadoramente em qualquer sistema', t => {
|
|
135
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
136
|
+
const alias = pointer.replace('/skills/', '/SKILLS/');
|
|
137
|
+
put(root, alias, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
138
|
+
git(root, 'add', '-f', '--', alias);
|
|
139
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
140
|
+
put(root, pointer, generated);
|
|
141
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
142
|
+
const aliasBytes = readFileSync(file(root, alias));
|
|
143
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
144
|
+
const ignore = readFileSync(file(root, '.gitignore'));
|
|
145
|
+
const preserved = () => {
|
|
146
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
147
|
+
assert.deepEqual(readFileSync(file(root, alias)), aliasBytes);
|
|
148
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
149
|
+
assert.deepEqual(readFileSync(file(root, '.gitignore')), ignore);
|
|
150
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
151
|
+
};
|
|
152
|
+
let check, error;
|
|
153
|
+
try { check = run(root, { check: true, force: true }); } catch (failure) { error = failure; }
|
|
154
|
+
if (error) assert.match(error.message, /maiusculas\/minusculas/i);
|
|
155
|
+
else assert.notEqual(check.exitCode, 0);
|
|
156
|
+
preserved();
|
|
157
|
+
assert.throws(() => run(root, { force: true }), /indice|maiusculas\/minusculas/i);
|
|
158
|
+
preserved();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('comparação de índice entende CRLF autorizado e recusa filtros, binários e divergência', t => {
|
|
162
|
+
const root = fixture(t), path = 'arquivo.txt';
|
|
163
|
+
const crlf = Buffer.from('texto\r\noutra linha\r\n'), lf = Buffer.from('texto\noutra linha\n');
|
|
164
|
+
git(root, 'config', 'core.autocrlf', 'false');
|
|
165
|
+
git(root, 'config', 'core.attributesfile', file(root, 'atributos-ausentes'));
|
|
166
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
167
|
+
git(root, 'config', 'core.autocrlf', 'true');
|
|
168
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), true);
|
|
169
|
+
assert.equal(indexContentMatches(root, path, crlf, Buffer.from('texto humano\noutra linha\n')), false);
|
|
170
|
+
assert.equal(indexContentMatches(root, path, Buffer.from([0, 13, 10]), Buffer.from([0, 10])), false);
|
|
171
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('um\r\ndois\r'), Buffer.from('um\ndois\r')), false);
|
|
172
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('\ufefftexto\r\n'), Buffer.from('texto\n')), false);
|
|
173
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('\ufefftexto\r\n'), Buffer.from('\ufefftexto\n')), true);
|
|
174
|
+
put(root, '.gitattributes', '*.txt -text\n');
|
|
175
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
176
|
+
put(root, '.gitattributes', '*.txt text filter=nao-executar\n');
|
|
177
|
+
git(root, 'config', 'filter.nao-executar.clean', 'exit 1');
|
|
178
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
179
|
+
put(root, '.gitattributes', '*.txt text working-tree-encoding=UTF-16\n');
|
|
180
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
181
|
+
put(root, '.gitattributes', '*.txt text ident\n');
|
|
182
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
183
|
+
put(root, '.gitattributes', '*.txt text eol=crlf\n');
|
|
184
|
+
git(root, 'config', 'core.autocrlf', 'false');
|
|
185
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('sync atualiza apontamento staged equivalente em repositório com autocrlf', t => {
|
|
189
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer), 'utf8').replaceAll('\n', '\r\n');
|
|
190
|
+
git(root, 'config', 'core.autocrlf', 'true');
|
|
191
|
+
put(root, pointer, generated);
|
|
192
|
+
git(root, 'add', '-f', '--', pointer);
|
|
193
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
194
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
195
|
+
assert.equal(run(root, { force: true }).exitCode, 0);
|
|
196
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
for (const target of ['.pbai4devs/state/derived.json', '.claude/.gitignore']) {
|
|
200
|
+
test(`índice divergente impede atualização de ${target}`, t => {
|
|
201
|
+
const root = fixture(t), generated = readFileSync(file(root, target));
|
|
202
|
+
put(root, target, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
203
|
+
git(root, 'add', '-f', '--', target);
|
|
204
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
205
|
+
put(root, target, generated);
|
|
206
|
+
if (target.endsWith('derived.json')) {
|
|
207
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
208
|
+
} else {
|
|
209
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache atualizado\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
|
|
210
|
+
}
|
|
211
|
+
assert.notEqual(run(root, { check: true, force: true }).exitCode, 0);
|
|
212
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
213
|
+
assert.deepEqual(readFileSync(file(root, target)), generated);
|
|
214
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const options of [{ check: true }, { force: true }]) {
|
|
219
|
+
test(`Git indisponível bloqueia limpeza em repositório (check=${Boolean(options.check)}, force=${Boolean(options.force)})`, t => {
|
|
220
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
221
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nConteúdo humano staged.\n')]));
|
|
222
|
+
git(root, 'add', '-f', '--', pointer);
|
|
223
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
224
|
+
put(root, pointer, generated);
|
|
225
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
226
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
227
|
+
withoutGit(t, () => assert.throws(() => run(root, options), /consultar o Git.*preservar/i));
|
|
228
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
229
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
230
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
test('projeto realmente sem Git pode limpar apontamento próprio sem executável Git', t => {
|
|
235
|
+
const root = fixture(t, false), human = '.claude/skills/criar-skill/equipe-vazia';
|
|
236
|
+
mkdirSync(file(root, human));
|
|
237
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
238
|
+
withoutGit(t, () => assert.equal(run(root, { force: true }).exitCode, 0));
|
|
239
|
+
assert.equal(existsSync(file(root, pointer)), false);
|
|
240
|
+
assert.ok(existsSync(file(root, human)));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('preserva diretório preexistente sem prova de propriedade após remover único apontamento', t => {
|
|
244
|
+
const root = fixture(t), directory = dirname(file(root, pointer));
|
|
245
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
246
|
+
assert.equal(run(root, { force: true }).exitCode, 0);
|
|
247
|
+
assert.equal(existsSync(file(root, pointer)), false);
|
|
248
|
+
assert.ok(existsSync(directory));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('alteração concorrente no índice bloqueia limpeza antes de qualquer exclusão', t => {
|
|
252
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
253
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
254
|
+
let staged;
|
|
255
|
+
assert.throws(() => synchronize({ root, localIgnores: false, generateEntrypoints: false, force: true,
|
|
256
|
+
output() {
|
|
257
|
+
if (staged !== undefined) return;
|
|
258
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nStaged pela equipe.\n')]));
|
|
259
|
+
git(root, 'add', '-f', '--', pointer);
|
|
260
|
+
staged = git(root, 'ls-files', '--stage', '-z');
|
|
261
|
+
put(root, pointer, generated);
|
|
262
|
+
},
|
|
263
|
+
}), /indice git mudou/i);
|
|
264
|
+
assert.notEqual(staged, undefined);
|
|
265
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
266
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), staged);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('índice divergente é protegido quando o projeto fica em subpasta do repositório Git', t => {
|
|
270
|
+
const outer = mkdtempSync(join(tmpdir(), 'sync-parent-git-'));
|
|
271
|
+
t.after(() => rmSync(outer, { recursive: true, force: true }));
|
|
272
|
+
git(outer, 'init', '--quiet');
|
|
273
|
+
const root = join(outer, 'projeto'); mkdirSync(root);
|
|
274
|
+
populate(root);
|
|
275
|
+
const generated = readFileSync(file(root, pointer));
|
|
276
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nStaged pela equipe.\n')]));
|
|
277
|
+
git(root, 'add', '-f', '--', pointer);
|
|
278
|
+
const index = git(outer, 'ls-files', '--stage', '-z');
|
|
279
|
+
put(root, pointer, generated);
|
|
280
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
281
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
282
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
283
|
+
assert.equal(git(outer, 'ls-files', '--stage', '-z'), index);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('conteúdo humano na pasta e apontamento gerenciado editado sobrevivem à limpeza forçada', t => {
|
|
287
|
+
const root = fixture(t), human = '.claude/skills/criar-skill/notas-da-equipe.md';
|
|
288
|
+
const notes = '# Referência própria\n', edited = Buffer.concat([readFileSync(file(root, pointer)), Buffer.from('\nNota própria.\n')]);
|
|
289
|
+
put(root, human, notes);
|
|
290
|
+
put(root, pointer, edited);
|
|
291
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
292
|
+
run(root, { force: true });
|
|
293
|
+
assert.equal(readFileSync(file(root, human), 'utf8'), notes);
|
|
294
|
+
assert.deepEqual(readFileSync(file(root, pointer)), edited);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('colisão com arquivo humano ancestral não é removida por --force', t => {
|
|
298
|
+
const root = fixture(t), ancestor = '.claude/skills/equipe-custom';
|
|
299
|
+
const human = 'Arquivo da equipe; não substituir.\n';
|
|
300
|
+
put(root, ancestor, human);
|
|
301
|
+
put(root, '.pbai4devs/skills/projeto/equipe-custom/SKILL.md', '---\nname: equipe-custom\ndescription: Exemplo.\n---\n\n# Equipe\n');
|
|
302
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
303
|
+
try { run(root, { force: true }); } catch (error) { assert.match(error.message, /conflito|gerenciado|arquivo|diretor/i); }
|
|
304
|
+
assert.equal(readFileSync(file(root, ancestor), 'utf8'), human);
|
|
305
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
306
|
+
});
|
|
@@ -13,6 +13,7 @@ import './LOCAL_IGNORE.mjs';
|
|
|
13
13
|
import './HUMAN_OWNERSHIP.mjs';
|
|
14
14
|
import './COEXISTENCE.mjs';
|
|
15
15
|
import './MANAGED_CONTENT.mjs';
|
|
16
|
+
import './PRESERVATION.mjs';
|
|
16
17
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
17
18
|
import { stateDirectory } from '../../lib/state-path.mjs';
|
|
18
19
|
|
|
@@ -140,7 +141,12 @@ test('sincroniza somente principais, preserva metadados e usa referências UTF-8
|
|
|
140
141
|
assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
|
|
141
142
|
assert.ok(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')));
|
|
142
143
|
for (const client of ['.claude', '.opencode', '.github', '.codex', '.cursor', '.kimi-code', '.vscode', '.agents', '.agent', '.gemini']) {
|
|
143
|
-
|
|
144
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) {
|
|
145
|
+
if (client === '.claude' && ['templates', 'scripts', 'tools', 'external-skills', 'state', 'tests', 'docs', 'adapters'].includes(kind)) {
|
|
146
|
+
assert.equal(existsSync(join(root, client, kind, 'demo/REFERENCE.md')), false);
|
|
147
|
+
assert.ok(existsSync(join(root, client, kind)), 'diretório sem prova de propriedade permanece');
|
|
148
|
+
} else assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
|
|
149
|
+
}
|
|
144
150
|
}
|
|
145
151
|
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
|
|
146
152
|
assert.ok(JSON.parse(read(root, '.pbai4devs/skills.json')).entries.some(entry => entry.path === '.pbai4devs/skills/grupo com espaço/outro'));
|
|
@@ -441,7 +447,8 @@ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para
|
|
|
441
447
|
rmSync(join(root, '.pbai4devs/rules/example/RULE.md'));
|
|
442
448
|
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf').replace('agente|.claude\n', 'agente|.claude|plano\n'));
|
|
443
449
|
run(root);
|
|
444
|
-
assert.equal(existsSync(join(root, '.claude/skills/projeto')), false);
|
|
450
|
+
assert.equal(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')), false);
|
|
451
|
+
assert.ok(existsSync(join(root, '.claude/skills/projeto')));
|
|
445
452
|
assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
446
453
|
assert.equal(existsSync(join(root, '.codex/rules/example.md')), false);
|
|
447
454
|
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, []);
|
|
@@ -882,7 +889,8 @@ test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t =
|
|
|
882
889
|
rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
|
|
883
890
|
run(root, { generateEntrypoints: true });
|
|
884
891
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
|
|
885
|
-
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
|
|
892
|
+
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')), false);
|
|
893
|
+
assert.ok(existsSync(join(root, '.agents/skills/projeto/demo')));
|
|
886
894
|
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
887
895
|
});
|
|
888
896
|
|