@pathbit/agentdevkit 0.0.23 → 0.0.25
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/revisar-entrega/COMMAND.md +6 -0
- package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +73 -4
- package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +112 -2
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +59 -11
- package/.pbai4devs/scripts/sincronizar-agentes/tests/CATALOG_OWNERSHIP.mjs +251 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +33 -13
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -3
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +65 -25
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +150 -5
- package/.pbai4devs/skills/git/git-merge/SKILL.md +24 -1
- package/CHANGELOG.md +15 -0
- package/docs/guides/installation.md +5 -3
- package/docs/guides/resources.md +4 -1
- package/docs/guides/troubleshooting.md +8 -6
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/install.mjs +100 -18
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { synchronize } from '../SCRIPT.mjs';
|
|
10
|
+
import { entrypointReference, mergeManagedContent } from '../../lib/managed-content.mjs';
|
|
11
|
+
import { mergeTomlContent } from '../../lib/managed-toml.mjs';
|
|
12
|
+
|
|
13
|
+
const catalogs = ['catalog.json', 'skills.json', 'agents.json', 'rules.json'].map(name => `.pbai4devs/${name}`);
|
|
14
|
+
const state = '.pbai4devs/state/derived.json';
|
|
15
|
+
const original = '.pbai4devs/skills/demo/SKILL.md';
|
|
16
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
17
|
+
const get = (root, path) => readFileSync(join(root, path));
|
|
18
|
+
const put = (root, path, bytes) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), bytes); };
|
|
19
|
+
const gitEnvironment = root => ({ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^GIT_/i.test(key))), GIT_CONFIG_GLOBAL: join(root, '.git-config-ausente'), GIT_CONFIG_NOSYSTEM: '1', GIT_OPTIONAL_LOCKS: '0' });
|
|
20
|
+
const sync = (root, options = {}) => {
|
|
21
|
+
// synchronize is synchronous and launches Git internally. Isolate only for
|
|
22
|
+
// this call so an inherited GIT_INDEX_FILE/GIT_DIR cannot touch another repo.
|
|
23
|
+
const saved = Object.fromEntries(Object.entries(process.env).filter(([key]) => /^GIT_/i.test(key)));
|
|
24
|
+
for (const key of Object.keys(saved)) delete process.env[key];
|
|
25
|
+
Object.assign(process.env, { GIT_CONFIG_GLOBAL: join(root, '.git-config-ausente'), GIT_CONFIG_NOSYSTEM: '1', GIT_OPTIONAL_LOCKS: '0' });
|
|
26
|
+
try { return synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options }); }
|
|
27
|
+
finally {
|
|
28
|
+
for (const key of Object.keys(process.env)) if (/^GIT_/i.test(key)) delete process.env[key];
|
|
29
|
+
Object.assign(process.env, saved);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function git(root, ...args) {
|
|
33
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', env: gitEnvironment(root) });
|
|
34
|
+
assert.equal(result.status, 0, result.stderr);
|
|
35
|
+
}
|
|
36
|
+
function fixture(t) {
|
|
37
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'adk-catalog-ownership-')));
|
|
38
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
39
|
+
git(root, 'init', '-q');
|
|
40
|
+
put(root, '.gitignore', 'node_modules/\n');
|
|
41
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
42
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache\n/*\n!/.gitignore\n{{ALLOWLIST}}\n');
|
|
43
|
+
put(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
|
|
44
|
+
put(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Shared\n/tools/*/.local/\n');
|
|
45
|
+
put(root, original, '---\nname: demo\ndescription: Demo.\n---\n\n# Demo\n');
|
|
46
|
+
git(root, 'add', '-f', '.gitignore');
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const path of catalogs) for (const force of [false, true]) {
|
|
51
|
+
test(`catálogo editado permanece intacto na sincronização direta: ${path}, force=${force}`, t => {
|
|
52
|
+
const root = fixture(t);
|
|
53
|
+
assert.equal(sync(root).exitCode, 0);
|
|
54
|
+
put(root, path, '{"human":"preservar"}\n');
|
|
55
|
+
git(root, 'add', '-f', '--', path);
|
|
56
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
57
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
58
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
59
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('catálogo anterior idêntico não é assumido como próprio sem estado', t => {
|
|
64
|
+
const root = fixture(t);
|
|
65
|
+
assert.equal(sync(root).exitCode, 0);
|
|
66
|
+
const recorded = JSON.parse(get(root, state));
|
|
67
|
+
delete recorded.catalogs;
|
|
68
|
+
put(root, state, `${JSON.stringify(recorded, null, 2)}\n`);
|
|
69
|
+
const before = Object.fromEntries([...catalogs, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
70
|
+
const result = sync(root, { force: true });
|
|
71
|
+
for (const path of catalogs) assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-catalog'), path);
|
|
72
|
+
assert.equal(JSON.parse(get(root, state)).catalogs, undefined);
|
|
73
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
74
|
+
put(root, original, '---\nname: demo\ndescription: Mudança humana.\n---\n\n# Demo\n');
|
|
75
|
+
assert.throws(() => sync(root, { force: true }), /Conteudo proprio preservado/);
|
|
76
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('sincronizações diretas sucessivas registram hashes dos catálogos criados', t => {
|
|
80
|
+
const root = fixture(t);
|
|
81
|
+
assert.equal(sync(root).exitCode, 0);
|
|
82
|
+
for (const path of catalogs) assert.equal(JSON.parse(get(root, state)).catalogs[path], digest(get(root, path)), path);
|
|
83
|
+
put(root, original, '---\nname: demo\ndescription: Nova descrição.\n---\n\n# Demo\n');
|
|
84
|
+
assert.equal(sync(root).exitCode, 0);
|
|
85
|
+
for (const path of catalogs) assert.equal(JSON.parse(get(root, state)).catalogs[path], digest(get(root, path)), path);
|
|
86
|
+
assert.equal(sync(root, { check: true }).exitCode, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('registro de catálogo desconhecido ou hash inválido bloqueia antes de alterar arquivos', t => {
|
|
90
|
+
const root = fixture(t);
|
|
91
|
+
assert.equal(sync(root).exitCode, 0);
|
|
92
|
+
const registry = JSON.parse(get(root, state));
|
|
93
|
+
registry.catalogs['.pbai4devs/other.json'] = 'a'.repeat(64);
|
|
94
|
+
put(root, state, JSON.stringify(registry));
|
|
95
|
+
const before = Object.fromEntries([...catalogs, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
96
|
+
assert.throws(() => sync(root, { force: true }), /Registro de catalogos derivados invalido/);
|
|
97
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
98
|
+
assert.ok(existsSync(join(root, original)));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (const path of ['.pbai4devs/.gitignore', '.claude/.gitignore']) for (const force of [false, true]) {
|
|
102
|
+
test(`bloco ignore editado permanece intacto: ${path}, force=${force}`, t => {
|
|
103
|
+
const root = fixture(t);
|
|
104
|
+
assert.equal(sync(root).exitCode, 0);
|
|
105
|
+
const content = get(root, path).toString('utf8').replace('# BEGIN AGENT IGNORE', '# BEGIN AGENT IGNORE\n# Regra humana\n!/conteudo-humano/');
|
|
106
|
+
put(root, path, content);
|
|
107
|
+
git(root, 'add', '-f', '--', path);
|
|
108
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
109
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
110
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
111
|
+
for (const [name, bytes] of Object.entries(before)) assert.deepEqual(get(root, name), bytes, name);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const path of ['.pbai4devs/.gitignore', '.claude/.gitignore']) {
|
|
116
|
+
test(`bloco ignore igual sem comprovante não é adotado: ${path}`, t => {
|
|
117
|
+
const root = fixture(t);
|
|
118
|
+
assert.equal(sync(root).exitCode, 0);
|
|
119
|
+
const registry = JSON.parse(get(root, state));
|
|
120
|
+
delete registry.blocks[path];
|
|
121
|
+
put(root, state, JSON.stringify(registry, null, 2) + '\n');
|
|
122
|
+
const originalIgnore = get(root, path);
|
|
123
|
+
const result = sync(root, { force: true });
|
|
124
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-ignore-block'));
|
|
125
|
+
assert.equal(JSON.parse(get(root, state)).blocks?.[path], undefined);
|
|
126
|
+
assert.deepEqual(get(root, path), originalIgnore);
|
|
127
|
+
const template = path.startsWith('.pbai4devs/') ? 'shared.ignore' : 'agents.ignore';
|
|
128
|
+
const templatePath = `.pbai4devs/templates/gitignore/${template}`;
|
|
129
|
+
put(root, templatePath, Buffer.concat([get(root, templatePath), Buffer.from('/novo-item/\n')]));
|
|
130
|
+
assert.throws(() => sync(root, { force: true }), /Conteudo proprio preservado/);
|
|
131
|
+
assert.deepEqual(get(root, path), originalIgnore);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test('bloco idêntico em AGENTS.md sem registro não é adotado', t => {
|
|
136
|
+
const root = fixture(t), path = 'AGENTS.md';
|
|
137
|
+
put(root, '.pbai4devs/templates/entrypoints/AGENTS.md', '# Instruções\n');
|
|
138
|
+
const content = mergeManagedContent('# Instruções humanas\n', entrypointReference(root, path)).text;
|
|
139
|
+
put(root, path, content);
|
|
140
|
+
const result = sync(root, { force: true, generateEntrypoints: true });
|
|
141
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-managed-block'));
|
|
142
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
143
|
+
const registry = JSON.parse(get(root, state));
|
|
144
|
+
assert.equal(registry.blocks?.[path], undefined);
|
|
145
|
+
assert.equal(registry.entries?.[path], undefined);
|
|
146
|
+
assert.equal(sync(root, { force: true, generateEntrypoints: true }).exitCode, 0);
|
|
147
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (const kind of ['markdown', 'toml']) test(`bloco nativo ${kind} idêntico sem registro não é adotado`, t => {
|
|
151
|
+
const root = fixture(t);
|
|
152
|
+
const path = kind === 'markdown' ? '.claude/skills/demo/SKILL.md' : '.gemini/commands/demo.toml';
|
|
153
|
+
let content;
|
|
154
|
+
if (kind === 'markdown') {
|
|
155
|
+
content = mergeManagedContent('# Instruções humanas\n', 'Leia e siga [origem](../../../.pbai4devs/skills/demo/SKILL.md).').text;
|
|
156
|
+
} else {
|
|
157
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\nagente|.gemini|plano\n');
|
|
158
|
+
put(root, '.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Demo.\n---\n\n# Demo\n');
|
|
159
|
+
content = mergeTomlContent('description = "Humano"\nprompt = "Meu prompt"\n', '@{.pbai4devs/commands/demo/COMMAND.md}').text;
|
|
160
|
+
}
|
|
161
|
+
put(root, path, content);
|
|
162
|
+
const result = sync(root, { force: true });
|
|
163
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-managed-block'));
|
|
164
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
165
|
+
const registry = JSON.parse(get(root, state));
|
|
166
|
+
assert.equal(registry.blocks?.[path], undefined);
|
|
167
|
+
assert.equal(registry.files?.[path], undefined);
|
|
168
|
+
assert.equal(sync(root, { force: true }).exitCode, 0);
|
|
169
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
for (const force of [false, true]) test(`configuração humana não é alterada nem adotada: force=${force}`, t => {
|
|
173
|
+
const root = fixture(t), path = '.pbai4devs/espelhos.conf';
|
|
174
|
+
const identical = get(root, path);
|
|
175
|
+
const result = sync(root, { force });
|
|
176
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-configuration'));
|
|
177
|
+
assert.equal(JSON.parse(get(root, state)).config, undefined);
|
|
178
|
+
assert.deepEqual(get(root, path), identical);
|
|
179
|
+
put(root, path, Buffer.concat([identical, Buffer.from('documento|manual-humano\n')]));
|
|
180
|
+
git(root, 'add', '-f', '--', path);
|
|
181
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
182
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
183
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
184
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('overlay de configuração inválido não autoriza escrita', t => {
|
|
188
|
+
const root = fixture(t);
|
|
189
|
+
assert.equal(sync(root).exitCode, 0);
|
|
190
|
+
const registry = JSON.parse(get(root, state));
|
|
191
|
+
registry.config = 'nao-e-sha256';
|
|
192
|
+
put(root, state, JSON.stringify(registry));
|
|
193
|
+
const before = get(root, state);
|
|
194
|
+
assert.throws(() => sync(root, { force: true }), /Registro de configuracao derivada invalido/);
|
|
195
|
+
assert.deepEqual(get(root, state), before);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('CLI reconhece catálogos da sincronização direta e preserva atualização de ignore', t => {
|
|
199
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'adk-cli-sync-ownership-')));
|
|
200
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
201
|
+
git(root, 'init', '-q');
|
|
202
|
+
put(root, '.gitignore', 'node_modules/\n');
|
|
203
|
+
put(root, 'AGENTS.md', '# Instruções existentes\n');
|
|
204
|
+
put(root, 'staged.txt', 'Conteúdo staged humano.\n');
|
|
205
|
+
git(root, 'add', '-f', '.gitignore', 'AGENTS.md', 'staged.txt');
|
|
206
|
+
const initial = Object.fromEntries(['.gitignore', '.git/index', 'staged.txt'].map(path => [path, get(root, path)]));
|
|
207
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../bin/agentdevkit.mjs');
|
|
208
|
+
function runCli(...args) {
|
|
209
|
+
const result = spawnSync(process.execPath, [cliPath, ...args, '--target', root, '--json'], {
|
|
210
|
+
cwd: root, env: gitEnvironment(root), encoding: 'utf8', timeout: 90000, maxBuffer: 16 * 1024 * 1024,
|
|
211
|
+
});
|
|
212
|
+
let output; try { output = JSON.parse(result.stdout); } catch { assert.fail(`Resposta CLI inválida: ${result.stderr} ${result.stdout}`); }
|
|
213
|
+
assert.equal(result.status, 0, `${args.join(' ')}: ${result.stderr} ${result.stdout}`);
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
216
|
+
function direct() {
|
|
217
|
+
const result = spawnSync(process.execPath, [join(root, '.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs')], {
|
|
218
|
+
cwd: root, env: gitEnvironment(root), encoding: 'utf8', timeout: 90000,
|
|
219
|
+
});
|
|
220
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
221
|
+
}
|
|
222
|
+
runCli('init', '--agents', 'codex');
|
|
223
|
+
assert.ok(get(root, 'AGENTS.md').toString('utf8').startsWith('# Instruções existentes\n'));
|
|
224
|
+
put(root, '.pbai4devs/skills/projeto/human-extra/SKILL.md', '---\nname: human-extra\ndescription: Minha skill.\n---\n\n# Minha skill\n');
|
|
225
|
+
direct(); direct();
|
|
226
|
+
assert.equal(runCli('doctor').ok, true);
|
|
227
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
228
|
+
assert.equal(runCli('update').ok, true);
|
|
229
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
230
|
+
// Simule uma versão legada que registrou legitimamente este manifesto.
|
|
231
|
+
const config = '.pbai4devs/espelhos.conf', installed = '.pbai4devs/state/agentdevkit.json';
|
|
232
|
+
put(root, config, Buffer.concat([get(root, config), Buffer.from('documento|legado-comprovado\n')]));
|
|
233
|
+
const previous = JSON.parse(get(root, installed));
|
|
234
|
+
previous.files[config] = digest(get(root, config));
|
|
235
|
+
put(root, installed, JSON.stringify(previous, null, 2) + '\n');
|
|
236
|
+
direct();
|
|
237
|
+
assert.doesNotMatch(get(root, config).toString('utf8'), /documento\|legado-comprovado/);
|
|
238
|
+
assert.equal(JSON.parse(get(root, state)).config, digest(get(root, config)));
|
|
239
|
+
assert.equal(runCli('doctor').ok, true);
|
|
240
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
241
|
+
assert.equal(runCli('update').ok, true);
|
|
242
|
+
const template = '.pbai4devs/templates/gitignore/shared.ignore';
|
|
243
|
+
const humanTemplate = Buffer.concat([get(root, template), Buffer.from('\n/artefato-humano/\n')]);
|
|
244
|
+
put(root, template, humanTemplate);
|
|
245
|
+
direct();
|
|
246
|
+
assert.match(get(root, '.pbai4devs/.gitignore').toString('utf8'), /\/artefato-humano\//);
|
|
247
|
+
assert.equal(runCli('update').ok, true);
|
|
248
|
+
assert.deepEqual(get(root, template), humanTemplate);
|
|
249
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
250
|
+
for (const [path, content] of Object.entries(initial)) assert.deepEqual(get(root, path), content, path);
|
|
251
|
+
});
|
|
@@ -14,6 +14,7 @@ import './HUMAN_OWNERSHIP.mjs';
|
|
|
14
14
|
import './COEXISTENCE.mjs';
|
|
15
15
|
import './MANAGED_CONTENT.mjs';
|
|
16
16
|
import './PRESERVATION.mjs';
|
|
17
|
+
import './CATALOG_OWNERSHIP.mjs';
|
|
17
18
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
18
19
|
import { stateDirectory } from '../../lib/state-path.mjs';
|
|
19
20
|
|
|
@@ -276,21 +277,22 @@ test('gera gitignore canonico e de clientes; registro gitignore nao espelha recu
|
|
|
276
277
|
assertCheckReadonly(root, {}, 0);
|
|
277
278
|
});
|
|
278
279
|
|
|
279
|
-
test('
|
|
280
|
+
test('preserva bloco gitignore editado e atualiza bloco conhecido sem perder regras externas', t => {
|
|
280
281
|
const root = fixture(t, true); run(root);
|
|
281
282
|
const prefix = '# Regras do usuario\r\n/private-cache/\r\n';
|
|
282
283
|
const suffix = '# Excecao local\r\n!keep.log\r\n';
|
|
284
|
+
const original = read(root, '.codex/.gitignore');
|
|
283
285
|
const old = `${prefix}${ignoreBegin}\r\n/custom-inside/\r\n${ignoreEnd}\r\n${suffix}`;
|
|
284
286
|
write(root, '.codex/.gitignore', old);
|
|
285
287
|
assertCheckReadonly(root); assertCheckReadonly(root, { force: true });
|
|
286
|
-
|
|
287
|
-
assert.equal(
|
|
288
|
-
|
|
289
|
-
assert.ok(repaired.startsWith(prefix)); assert.ok(repaired.endsWith(suffix));
|
|
290
|
-
assert.ok(!repaired.includes('/custom-inside/')); assert.ok(repaired.includes('/cache/'));
|
|
288
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
289
|
+
assert.equal(read(root, '.codex/.gitignore'), old);
|
|
290
|
+
write(root, '.codex/.gitignore', `${prefix}${original}${suffix}`);
|
|
291
291
|
write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Novo padrao\n{{ALLOWLIST}}\n/cache/\n/session/\n');
|
|
292
292
|
assertCheckReadonly(root);
|
|
293
|
-
run(root);
|
|
293
|
+
const result = run(root, { force: true });
|
|
294
|
+
assert.equal(readFileSync(join(result.backupRoot, '.codex/.gitignore'), 'utf8'), `${prefix}${original}${suffix}`);
|
|
295
|
+
assert.ok(read(root, '.codex/.gitignore').startsWith(prefix));
|
|
294
296
|
assert.ok(read(root, '.codex/.gitignore').includes('/session/'));
|
|
295
297
|
assert.ok(readFileSync(join(root, '.codex/.gitignore'), 'utf8').endsWith(suffix));
|
|
296
298
|
assertCheckReadonly(root, {}, 0);
|
|
@@ -374,6 +376,9 @@ test('registros são derivados, determinísticos e --check nunca os corrige', t
|
|
|
374
376
|
run(root);
|
|
375
377
|
write(root, '.pbai4devs/skills.json', '{"incorreto":true}\n');
|
|
376
378
|
assertCheckReadonly(root);
|
|
379
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
380
|
+
assert.equal(read(root, '.pbai4devs/skills.json'), '{"incorreto":true}\n');
|
|
381
|
+
rmSync(join(root, '.pbai4devs/skills.json'));
|
|
377
382
|
run(root);
|
|
378
383
|
assert.ok(Array.isArray(JSON.parse(read(root, '.pbai4devs/skills.json')).entries));
|
|
379
384
|
rmSync(join(root, '.pbai4devs/adapters/antigravity/agents/revisor.md'));
|
|
@@ -455,7 +460,7 @@ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para
|
|
|
455
460
|
assertCheckReadonly(root, {}, 0);
|
|
456
461
|
});
|
|
457
462
|
|
|
458
|
-
test('
|
|
463
|
+
test('preserva linhas legadas humanas sem prova de propriedade', t => {
|
|
459
464
|
const root = fixture(t);
|
|
460
465
|
run(root);
|
|
461
466
|
write(root, 'docs/produto/prd/demo/PRD.md', '# Produto real\n');
|
|
@@ -463,13 +468,26 @@ test('remove linhas legadas e preserva arquivos sem prova de propriedade', t =>
|
|
|
463
468
|
write(root, '.claude/manual/DOC.md', pointer('../../manual/guia%20com%20espa%C3%A7o/DOC.md'));
|
|
464
469
|
write(root, '.github/prompts/missing.prompt.md', pointer());
|
|
465
470
|
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'documento|manual\n' + 'nativo|.github/prompts/missing.prompt.md|.pbai4devs/commands/missing/COMMAND.md\n');
|
|
471
|
+
const config = read(root, '.pbai4devs/espelhos.conf');
|
|
466
472
|
assertCheckReadonly(root);
|
|
467
|
-
run(root);
|
|
468
|
-
assert.
|
|
473
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
474
|
+
assert.equal(read(root, '.pbai4devs/espelhos.conf'), config);
|
|
469
475
|
assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), true);
|
|
470
476
|
assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), true);
|
|
471
477
|
assert.ok(existsSync(join(root, 'docs/produto/prd/demo/PRD.md')));
|
|
472
478
|
assert.ok(existsSync(join(root, 'manual/guia com espaço/DOC.md')));
|
|
479
|
+
assertCheckReadonly(root);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('limpa configuração legada comprovadamente gerenciada e registra novo hash', t => {
|
|
483
|
+
const root = fixture(t);
|
|
484
|
+
const path = '.pbai4devs/espelhos.conf';
|
|
485
|
+
write(root, path, read(root, path) + 'documento|manual\n');
|
|
486
|
+
recordOwned(root, [path]);
|
|
487
|
+
run(root);
|
|
488
|
+
assert.doesNotMatch(read(root, path), /documento\|/);
|
|
489
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
490
|
+
assert.equal(derived.config, createHash('sha256').update(readFileSync(join(root, path))).digest('hex'));
|
|
473
491
|
assertCheckReadonly(root, {}, 0);
|
|
474
492
|
});
|
|
475
493
|
|
|
@@ -745,13 +763,15 @@ test('criar editar e remover recursos atualiza todos os tipos e os índices nati
|
|
|
745
763
|
assertCheckReadonly(root, {}, 0);
|
|
746
764
|
});
|
|
747
765
|
|
|
748
|
-
test('JSON derivado
|
|
766
|
+
test('JSON derivado alterado é preservado até remoção deliberada pelo usuário', t => {
|
|
749
767
|
const root = completeCatalogFixture(t);
|
|
750
768
|
run(root);
|
|
751
769
|
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) write(root, path, '{corrompido');
|
|
752
770
|
assertCheckReadonly(root, { force: true });
|
|
753
|
-
|
|
754
|
-
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(
|
|
771
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
772
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(root, path), '{corrompido');
|
|
773
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) rmSync(join(root, path));
|
|
774
|
+
run(root);
|
|
755
775
|
assert.equal(JSON.parse(read(root, '.pbai4devs/catalog.json')).schemaVersion, 1);
|
|
756
776
|
assertCheckReadonly(root, {}, 0);
|
|
757
777
|
});
|
|
@@ -29,21 +29,30 @@ execuções e projetos anteriores não são substituídos nem removidos. Cada
|
|
|
29
29
|
exemplo recebe Git local, sem commits nem identidade alterada. O diretório
|
|
30
30
|
gerado contém um ignore próprio, inclusive em projetos consumidores. Se a
|
|
31
31
|
preparação falhar, o índice da nova execução permanece em `preparing`.
|
|
32
|
+
O gerador verifica links e identidade da saída antes e depois das etapas, mas
|
|
33
|
+
isso não é uma proteção atômica. Um processo concorrente com permissão para
|
|
34
|
+
trocar diretórios ainda pode redirecionar uma escrita entre as verificações;
|
|
35
|
+
execute-o em uma área sem esse acesso concorrente.
|
|
32
36
|
O hub `.pbai4devs` e os clientes registrados são copiados, incluindo comandos
|
|
33
37
|
nativos em TOML. Sem manifesto local, os templates definem os clientes em
|
|
34
38
|
memória, respeitando a seleção salva. `.vscode` exige registro. Apenas as entradas
|
|
35
39
|
`AGENTS.md`, `CLAUDE.md` e `GEMINI.md` presentes na origem são incluídas na raiz.
|
|
36
40
|
Os templates substituem somente entradas com blocos gerenciados válidos;
|
|
37
41
|
entradas próprias ou editadas são preservadas. Sem estado de instalação,
|
|
38
|
-
|
|
42
|
+
as entradas existentes são copiadas sem presumir propriedade do kit.
|
|
39
43
|
Os testes resolvem cada apontamento pelo formato declarado do cliente e recurso.
|
|
40
44
|
|
|
41
|
-
As cópias
|
|
45
|
+
As cópias guardam os catálogos originais em `.agent-tests/testar-maturidade/original-catalogs/`
|
|
46
|
+
e geram catálogos operacionais novos, sem alterar os arquivos da origem.
|
|
47
|
+
Arquivos da origem substituídos pela fixture ficam em `original-files/` nessa
|
|
48
|
+
mesma pasta. O relatório identifica cada cópia: o cenário não é um teste do
|
|
49
|
+
projeto original no local.
|
|
42
50
|
Não copiam o código do produto em `bin/`, `src/`, `scripts/` ou `tests/`,
|
|
43
51
|
nem essas pastas na localização antiga dentro do hub.
|
|
44
52
|
Executam sincronização/reparo/idempotência, catálogo, verificação de
|
|
45
53
|
recursos, diagnóstico, inspeção e gates negativos/positivo. Em dois casos,
|
|
46
|
-
criação
|
|
54
|
+
criação, edição, renomeação, remoção e preservação de índices alterados
|
|
55
|
+
exercitam o ciclo todo.
|
|
47
56
|
Aplicações executam testes HTTP reais em
|
|
48
57
|
portas efêmeras, encerradas ao fim. Legado e monorepo preservam seus arquivos.
|
|
49
58
|
Ferramentas copiadas ficam sem downloads: o check negativo é esperado.
|
|
@@ -15,6 +15,7 @@ const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
|
15
15
|
const owner = 'testar-maturidade';
|
|
16
16
|
const marker = '.maturity-case.json';
|
|
17
17
|
const suiteMarker = '.maturity-suite.json';
|
|
18
|
+
const generatedCatalogs = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']);
|
|
18
19
|
const outputIgnore = '# Exemplos gerados permanecem locais.\n*\n';
|
|
19
20
|
const defaultOutput = root => join(root, 'examples/generated');
|
|
20
21
|
const prepareResources = root => {
|
|
@@ -58,6 +59,7 @@ function createChild(root, parent, name, { allowExisting = false } = {}) {
|
|
|
58
59
|
noLinks(root, join(parent, name));
|
|
59
60
|
const output = stat(join(parent, name));
|
|
60
61
|
if (!output?.isDirectory() || output.isSymbolicLink()) fail('Diretório de saída inválido.');
|
|
62
|
+
return output;
|
|
61
63
|
}
|
|
62
64
|
function ensureDirectories(root, target) {
|
|
63
65
|
let current = root;
|
|
@@ -144,7 +146,7 @@ function copyAllowed(relativePath, directory = false) {
|
|
|
144
146
|
return true;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
export function copyAgentStructure(sourceRoot, destination) {
|
|
149
|
+
export function copyAgentStructure(sourceRoot, destination, { archiveCatalogs = false, archiveOverrides = new Set(), archiveEntrypoints = false } = {}) {
|
|
148
150
|
const roots = ['.pbai4devs', ...registeredRoots(sourceRoot)];
|
|
149
151
|
const stateFile = join(sourceRoot, stateDirectory(sourceRoot), 'agentdevkit.json');
|
|
150
152
|
const stateInfo = stat(stateFile);
|
|
@@ -152,7 +154,7 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
152
154
|
const installation = stateInfo ? json(stateFile) : null;
|
|
153
155
|
if (stateInfo && (!installation || !installation.blocks || typeof installation.blocks !== 'object' || Array.isArray(installation.blocks))) fail('Estado da instalação deve registrar os blocos gerenciados.');
|
|
154
156
|
function managedEntry(name) {
|
|
155
|
-
if (!installation) return
|
|
157
|
+
if (!installation) return false;
|
|
156
158
|
const expected = installation.blocks[name];
|
|
157
159
|
if (typeof expected !== 'string') return false;
|
|
158
160
|
const content = readFileSync(join(sourceRoot, name), 'utf8');
|
|
@@ -161,7 +163,7 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
161
163
|
const start = content.indexOf(begin), finish = content.indexOf(end);
|
|
162
164
|
return finish > start && digest(content.slice(start, finish + end.length)) === expected;
|
|
163
165
|
}
|
|
164
|
-
const files = []; const entrypoints = {}; let skipped = 0;
|
|
166
|
+
const files = []; const entrypoints = {}; const catalogSnapshots = []; const overriddenSnapshots = []; const seenSources = new Set(); let skipped = 0;
|
|
165
167
|
function copy(file, targetFile = file) {
|
|
166
168
|
const source = join(sourceRoot, file);
|
|
167
169
|
const item = stat(source); if (!item) return;
|
|
@@ -171,6 +173,14 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
171
173
|
mkdirSync(join(destination, targetFile), { recursive: true });
|
|
172
174
|
for (const name of readdirSync(source).sort()) copy(join(file, name), join(targetFile, name));
|
|
173
175
|
} else if (item.isFile()) {
|
|
176
|
+
seenSources.add(portable(file));
|
|
177
|
+
if (archiveCatalogs && generatedCatalogs.has(portable(file))) {
|
|
178
|
+
targetFile = `.agent-tests/testar-maturidade/original-catalogs/${basename(file)}`;
|
|
179
|
+
catalogSnapshots.push({ source: portable(file), snapshot: targetFile });
|
|
180
|
+
} else if (archiveOverrides.has(portable(file))) {
|
|
181
|
+
targetFile = `.agent-tests/testar-maturidade/original-files/${portable(file)}`;
|
|
182
|
+
overriddenSnapshots.push({ source: portable(file), snapshot: targetFile });
|
|
183
|
+
}
|
|
174
184
|
const target = join(destination, targetFile); mkdirSync(dirname(target), { recursive: true }); copyFileSync(source, target); files.push(portable(targetFile));
|
|
175
185
|
} else fail(`Arquivo não regular na origem: ${file}`);
|
|
176
186
|
}
|
|
@@ -183,11 +193,29 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
183
193
|
const selected = managedEntry(name) && stat(join(sourceRoot, template)) ? template : name;
|
|
184
194
|
const info = stat(join(sourceRoot, selected));
|
|
185
195
|
if (info && !info.isFile()) fail(`Entrada de agente deve ser arquivo regular: ${selected}`);
|
|
196
|
+
if (archiveEntrypoints && selected !== name) {
|
|
197
|
+
const snapshot = `.agent-tests/testar-maturidade/original-files/${name}`;
|
|
198
|
+
copy(name, snapshot); overriddenSnapshots.push({ source: name, snapshot });
|
|
199
|
+
}
|
|
186
200
|
copy(selected, name);
|
|
187
201
|
if (info) entrypoints[name] = selected;
|
|
188
202
|
}
|
|
203
|
+
// A fixture can create files belonging to an unselected client; archive
|
|
204
|
+
// those source files as well, without traversing symbolic-link ancestors.
|
|
205
|
+
for (const path of archiveOverrides) if (!seenSources.has(path)) {
|
|
206
|
+
noLinks(sourceRoot, join(sourceRoot, path));
|
|
207
|
+
copy(path);
|
|
208
|
+
}
|
|
189
209
|
const sourceDigest = digest(files.map(file => `${file}\0${digest(readFileSync(join(destination, file)))}`).join('\n'));
|
|
190
|
-
return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
|
|
210
|
+
return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, catalogSnapshots, overriddenSnapshots, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function fixtureOverrides(scenario, sourceRoot) {
|
|
214
|
+
const paths = ['README.md', '.pbai4devs/.gitignore', ...registeredRoots(sourceRoot).map(root => `${root}/.gitignore`), '.pbai4devs/state/configuracao/ESTADO.md', '.pbai4devs/estado/configuracao/ESTADO.md'];
|
|
215
|
+
if (scenario.planning !== 'none' || scenario.app) paths.push('.pbai4devs/state/README.md', '.pbai4devs/estado/README.md');
|
|
216
|
+
if (scenario.configured) paths.push('.dockerignore', '.env.example', 'Makefile');
|
|
217
|
+
if (scenario.layout === 'legacy') paths.push('Makefile', '.github/workflows/legacy.yml', '.vscode/settings.json');
|
|
218
|
+
return new Set(paths);
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
function planning(root, scenario) {
|
|
@@ -299,33 +327,41 @@ export function prepareExamples({ sourceRoot = projectRoot, outputRoot, scenario
|
|
|
299
327
|
const parent = outputRoot === undefined ? base : dirname(base);
|
|
300
328
|
ensureDirectories(sourceRoot, parent);
|
|
301
329
|
const name = outputRoot === undefined ? `maturity-${randomUUID()}` : basename(base);
|
|
302
|
-
createChild(sourceRoot, parent, name);
|
|
330
|
+
const created = createChild(sourceRoot, parent, name);
|
|
303
331
|
outputRoot = join(parent, name);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
332
|
+
const canonicalOutput = realpathSync(outputRoot);
|
|
333
|
+
const verifyOutput = (path = outputRoot) => {
|
|
334
|
+
noLinks(sourceRoot, path);
|
|
335
|
+
const current = stat(outputRoot);
|
|
336
|
+
if (!current?.isDirectory() || current.dev !== created.dev || current.ino !== created.ino || realpathSync(outputRoot) !== canonicalOutput) fail('Diretório de saída alterado durante a execução.');
|
|
337
|
+
};
|
|
338
|
+
const guarded = (path, operation) => { verifyOutput(path); const value = operation(); verifyOutput(path); return value; };
|
|
339
|
+
guarded(outputRoot, () => write(outputRoot, suiteMarker, JSON.stringify({ owner, version: 1 }, null, 2) + '\n'));
|
|
340
|
+
guarded(outputRoot, () => write(outputRoot, '.gitignore', outputIgnore));
|
|
341
|
+
guarded(outputRoot, () => writeIndex(outputRoot, [], 'preparing'));
|
|
307
342
|
const examples = [];
|
|
308
343
|
for (const scenario of scenarios) {
|
|
309
344
|
const destination = join(outputRoot, scenario.id);
|
|
310
|
-
mkdirSync(destination);
|
|
311
|
-
write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n');
|
|
312
|
-
const copied = copyAgentStructure(sourceRoot, destination);
|
|
313
|
-
planning(destination, scenario);
|
|
314
|
-
const app = application(destination, scenario);
|
|
315
|
-
write(destination, 'README.md', document(scenario.title, `${scenario.note}\n\nEste é um exemplo gerado. Estados de aprovação/publicação são dados de teste.\nRevisão do gate é simulação contratual, não revisão humana.\n\nOs scripts dos agentes foram copiados, sem pacotes ou credenciais conhecidas.\nO filtro não detecta segredos em conteúdo arbitrário.\n${app.tests.length ? `\nPara repetir os testes: \`${scenario.layout === 'legacy' ? 'npm run test:legacy' : 'npm test'}\`.\nO harness executa os testes Node diretamente, sem instalar dependências.\nO teste abre HTTP em porta efêmera e encerra o servidor.\n` : '\nNão há aplicação; a prova real exercita os scripts copiados.\n'}`, `fase: ${scenario.expected}\n`));
|
|
316
|
-
renderResources(destination);
|
|
317
|
-
if (initializeGit) gitInit(destination);
|
|
345
|
+
guarded(outputRoot, () => mkdirSync(destination));
|
|
346
|
+
guarded(destination, () => write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n'));
|
|
347
|
+
const copied = guarded(destination, () => copyAgentStructure(sourceRoot, destination, { archiveCatalogs: true, archiveOverrides: fixtureOverrides(scenario, sourceRoot), archiveEntrypoints: true }));
|
|
348
|
+
guarded(destination, () => planning(destination, scenario));
|
|
349
|
+
const app = guarded(destination, () => application(destination, scenario));
|
|
350
|
+
guarded(destination, () => write(destination, 'README.md', document(scenario.title, `${scenario.note}\n\nEste é um exemplo gerado, não um teste no projeto original. Estados de aprovação/publicação são dados de teste.\nRevisão do gate é simulação contratual, não revisão humana.\nCatálogos originais e arquivos substituídos pela fixture ficam em \`.agent-tests/testar-maturidade/\`; os catálogos operacionais são regenerados.\n\nOs scripts dos agentes foram copiados, sem pacotes ou credenciais conhecidas.\nO filtro não detecta segredos em conteúdo arbitrário.\n${app.tests.length ? `\nPara repetir os testes: \`${scenario.layout === 'legacy' ? 'npm run test:legacy' : 'npm test'}\`.\nO harness executa os testes Node diretamente, sem instalar dependências.\nO teste abre HTTP em porta efêmera e encerra o servidor.\n` : '\nNão há aplicação; a prova real exercita os scripts copiados.\n'}`, `fase: ${scenario.expected}\n`)));
|
|
351
|
+
guarded(destination, () => renderResources(destination));
|
|
352
|
+
if (initializeGit) guarded(destination, () => gitInit(destination));
|
|
318
353
|
examples.push({ ...scenario, root: destination, copied, application: app });
|
|
319
354
|
}
|
|
320
|
-
write(outputRoot, 'REPORT.json', JSON.stringify({ owner, version: 1, status: 'prepared', scenarios: [...ids], results: [] }, null, 2) + '\n');
|
|
321
|
-
writeIndex(outputRoot, examples, 'prepared');
|
|
355
|
+
guarded(outputRoot, () => write(outputRoot, 'REPORT.json', JSON.stringify({ owner, version: 1, status: 'prepared', scenarios: [...ids], copies: examples.map(example => ({ id: example.id, catalogSnapshots: example.copied.catalogSnapshots, overriddenSnapshots: example.copied.overriddenSnapshots })), results: [] }, null, 2) + '\n'));
|
|
356
|
+
guarded(outputRoot, () => writeIndex(outputRoot, examples, 'prepared'));
|
|
357
|
+
Object.defineProperty(examples, 'verifyOutput', { value: verifyOutput });
|
|
322
358
|
return examples;
|
|
323
359
|
}
|
|
324
360
|
|
|
325
361
|
function writeIndex(outputRoot, examples, status) {
|
|
326
362
|
const rows = examples.map(item => `| [${item.id}](${item.id}/README.md) | ${item.expected} | ${item.status || 'prepared'} |`).join('\n');
|
|
327
363
|
const progress = status === 'preparing' ? '\nPreparação em andamento nesta execução.\n' : '';
|
|
328
|
-
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nCada execução usa uma pasta nova e preserva as anteriores.\n`, `status: ${status}\n`));
|
|
364
|
+
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes; não são testes no projeto original.\nCatálogos operacionais são regenerados e suas cópias originais ficam em cada \`.agent-tests/testar-maturidade/\`.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nCada execução usa uma pasta nova e preserva as anteriores.\n`, `status: ${status}\n`));
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
function hashes(root, files) { return Object.fromEntries(files.map(file => [file, digest(readFileSync(join(root, file)))])); }
|
|
@@ -399,13 +435,14 @@ function resourceLifecycle(example, run) {
|
|
|
399
435
|
if (readFileSync(join(root, catalogPath), 'utf8') !== before.catalog || readFileSync(join(root, registryPath), 'utf8') !== before.registry) fail('O ciclo não restaurou os catálogos originais.');
|
|
400
436
|
const corrupted = '{"tampered":true}\n'; write(root, catalogPath, corrupted);
|
|
401
437
|
run(sync, ['--check'], 1, 'catalog-drift-detected');
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
if (
|
|
438
|
+
const blocked = run(sync, ['--force'], 2, 'catalog-drift-preserved');
|
|
439
|
+
if (!blocked.output.includes('Conteudo proprio preservado')) fail('A colisão não informou a preservação do catálogo.');
|
|
440
|
+
if (readFileSync(join(root, catalogPath), 'utf8') !== corrupted) fail('Sincronização alterou o JSON sintético sem ownership.');
|
|
441
|
+
write(root, catalogPath, before.catalog); // Restore only the content this fixture deliberately changed.
|
|
405
442
|
rmSync(join(root, catalogPath)); run(sync, ['--check'], 1, 'catalog-missing-detected');
|
|
406
443
|
run(sync, [], 0, 'catalog-missing-repaired'); run(sync, ['--check'], 0, 'lifecycle-clean');
|
|
407
444
|
if (readFileSync(join(root, catalogPath), 'utf8') !== before.catalog || readFileSync(join(root, registryPath), 'utf8') !== before.registry) fail('Catálogos não foram restaurados após corrupção/remoção.');
|
|
408
|
-
return { status: 'passed', operations: ['create', 'edit-description', 'rename', 'delete', '
|
|
445
|
+
return { status: 'passed', operations: ['create', 'edit-description', 'rename', 'delete', 'protect-modified-json', 'remove-json'], clients: rows.length, sourceRestored: true };
|
|
409
446
|
}
|
|
410
447
|
|
|
411
448
|
function reviewContract(example, run, proof) {
|
|
@@ -444,7 +481,7 @@ function reviewContract(example, run, proof) {
|
|
|
444
481
|
export function testExample(example) {
|
|
445
482
|
const commands = []; const logRoot = '.agent-tests/maturity';
|
|
446
483
|
const run = runner(example.root, logRoot, commands);
|
|
447
|
-
const result = { id: example.id, expected: example.expected, status: 'failed', copied: { files: example.copied.copiedFiles, roots: example.copied.roots, skipped: example.copied.skipped, sourceDigest: example.copied.sourceDigest, entrypoints: example.copied.entrypoints }, commands, limits: ['Executado somente no SO/runtime deste host.', 'Estado de produto e revisão do gate são dados sintéticos.', 'Build Docker, UI e implantação não são exercitados.', 'Cópia exclui credenciais por nomes conhecidos, sem varredura semântica de segredos.'] };
|
|
484
|
+
const result = { id: example.id, expected: example.expected, status: 'failed', copied: { files: example.copied.copiedFiles, roots: example.copied.roots, skipped: example.copied.skipped, sourceDigest: example.copied.sourceDigest, entrypoints: example.copied.entrypoints, catalogSnapshots: example.copied.catalogSnapshots, overriddenSnapshots: example.copied.overriddenSnapshots }, commands, limits: ['Cenário sintético: não testa o projeto original no local.', 'Executado somente no SO/runtime deste host.', 'Estado de produto e revisão do gate são dados sintéticos.', 'Build Docker, UI e implantação não são exercitados.', 'Cópia exclui credenciais por nomes conhecidos, sem varredura semântica de segredos.'] };
|
|
448
485
|
const preserved = hashes(example.root, example.application.protected);
|
|
449
486
|
try {
|
|
450
487
|
const sync = '.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
@@ -493,11 +530,14 @@ export function testExample(example) {
|
|
|
493
530
|
|
|
494
531
|
export function runExamples(options = {}) {
|
|
495
532
|
const examples = prepareExamples(options);
|
|
496
|
-
const results = examples.map(example => { const result = testExample(example); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
|
|
533
|
+
const results = examples.map(example => { examples.verifyOutput(example.root); const result = testExample(example); examples.verifyOutput(example.root); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
|
|
497
534
|
const outputRoot = dirname(examples[0].root);
|
|
498
535
|
const report = { owner, version: 1, status: results.every(item => item.status === 'passed') ? 'passed' : 'failed', platform: process.platform, arch: process.arch, node: process.version, results, limits: ['Provas reais somente das operações descritas e APIs locais.', 'Pareceres do gate são simulações explícitas, sem revisão humana.', 'Não certifica produção nem sistemas operacionais não executados.'] };
|
|
536
|
+
examples.verifyOutput();
|
|
499
537
|
write(outputRoot, 'REPORT.json', JSON.stringify(report, null, 2) + '\n');
|
|
538
|
+
examples.verifyOutput();
|
|
500
539
|
writeIndex(outputRoot, examples.map(example => ({ ...example, status: results.find(result => result.id === example.id).status })), report.status);
|
|
540
|
+
examples.verifyOutput();
|
|
501
541
|
return { outputRoot, report };
|
|
502
542
|
}
|
|
503
543
|
|