@orkastery/cli 0.6.4 → 0.6.5

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/dist/junit.js ADDED
@@ -0,0 +1,546 @@
1
+ "use strict";
2
+ /**
3
+ * RM-081 F1: o subconjunto de JUnit XML que o verify usa como evidencia nominal.
4
+ *
5
+ * O parser e deliberadamente pequeno: valida o balanceamento do XML, nao aceita DTD e so
6
+ * interpreta suites, casos e os tres marcadores de resultado. Saida, propriedades e mensagens
7
+ * continuam opacas. O codigo de saida do comando permanece a autoridade do veredito.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.LIMITE_JUNIT_BYTES = void 0;
44
+ exports.lerJUnitXml = lerJUnitXml;
45
+ exports.caminhoJUnitDoComando = caminhoJUnitDoComando;
46
+ exports.marcacaoDeRelogioDoComando = marcacaoDeRelogioDoComando;
47
+ exports.prepararRelatorioJUnit = prepararRelatorioJUnit;
48
+ exports.lerRelatorioJUnit = lerRelatorioJUnit;
49
+ exports.anexarRelatorioJUnit = anexarRelatorioJUnit;
50
+ const fs = __importStar(require("node:fs"));
51
+ const path = __importStar(require("node:path"));
52
+ const node_util_1 = require("node:util");
53
+ const policies_1 = require("./policies");
54
+ const redacao_url_1 = require("./redacao-url");
55
+ /** Relatorio acima deste teto e evidencia invalida, sem tentar alocar o XML inteiro. */
56
+ exports.LIMITE_JUNIT_BYTES = 16 * 1024 * 1024;
57
+ /** Sistemas de arquivos podem arredondar mtime; a remocao previa continua sendo a barreira principal. */
58
+ const TOLERANCIA_MTIME_MS = 2_000;
59
+ const NOME_XML = /^[A-Za-z_][A-Za-z0-9_.:-]*/;
60
+ const ENTIDADES = {
61
+ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'",
62
+ };
63
+ function erro(detalhe) {
64
+ throw new Error(`junit.xml.invalido: ${detalhe}`);
65
+ }
66
+ function decodificar(texto) {
67
+ for (let i = texto.indexOf('&'); i >= 0; i = texto.indexOf('&', i)) {
68
+ const entidade = /^&([A-Za-z][A-Za-z0-9]*);/.exec(texto.slice(i));
69
+ if (!entidade || !Object.hasOwn(ENTIDADES, entidade[1])) {
70
+ return erro('entidade XML nao aceita');
71
+ }
72
+ i += entidade[0].length;
73
+ }
74
+ return texto.replace(/&([^;]+);/g, (_tudo, nome) => ENTIDADES[nome] ?? erro(`entidade &${nome}; nao aceita`));
75
+ }
76
+ function codigoDoErro(e) {
77
+ const codigo = e?.code;
78
+ return typeof codigo === 'string' && /^[A-Z0-9_]+$/.test(codigo) ? codigo : 'DESCONHECIDO';
79
+ }
80
+ function nomeLocal(nome) {
81
+ return nome.slice(nome.lastIndexOf(':') + 1);
82
+ }
83
+ function fimDaTag(xml, inicio) {
84
+ let aspas = null;
85
+ for (let i = inicio; i < xml.length; i++) {
86
+ const c = xml[i];
87
+ if (aspas) {
88
+ if (c === aspas)
89
+ aspas = null;
90
+ continue;
91
+ }
92
+ if (c === '"' || c === "'") {
93
+ aspas = c;
94
+ continue;
95
+ }
96
+ if (c === '>')
97
+ return i;
98
+ }
99
+ return erro('tag sem fechamento');
100
+ }
101
+ function lerInicio(bruto) {
102
+ let texto = bruto.trim();
103
+ const autocontido = texto.endsWith('/');
104
+ if (autocontido)
105
+ texto = texto.slice(0, -1).trimEnd();
106
+ const achado = NOME_XML.exec(texto);
107
+ if (!achado)
108
+ return erro('nome de elemento ausente');
109
+ const nome = achado[0];
110
+ const atributos = {};
111
+ let i = nome.length;
112
+ while (i < texto.length) {
113
+ while (/\s/.test(texto[i] ?? ''))
114
+ i++;
115
+ if (i >= texto.length)
116
+ break;
117
+ const atributo = NOME_XML.exec(texto.slice(i));
118
+ if (!atributo)
119
+ return erro(`atributo invalido em <${nome}>`);
120
+ const chave = atributo[0];
121
+ i += chave.length;
122
+ while (/\s/.test(texto[i] ?? ''))
123
+ i++;
124
+ if (texto[i++] !== '=')
125
+ return erro(`atributo ${chave} sem =`);
126
+ while (/\s/.test(texto[i] ?? ''))
127
+ i++;
128
+ const aspas = texto[i++];
129
+ if (aspas !== '"' && aspas !== "'")
130
+ return erro(`atributo ${chave} sem aspas`);
131
+ const fim = texto.indexOf(aspas, i);
132
+ if (fim < 0)
133
+ return erro(`atributo ${chave} sem fechamento`);
134
+ if (Object.hasOwn(atributos, chave))
135
+ return erro(`atributo ${chave} repetido`);
136
+ atributos[chave] = decodificar(texto.slice(i, fim)).replace(/[\t\n]/g, ' ');
137
+ i = fim + 1;
138
+ }
139
+ return { nome, atributos, autocontido };
140
+ }
141
+ function duracao(atributos) {
142
+ if (atributos.time === undefined || atributos.time === '')
143
+ return 0;
144
+ const segundos = Number(atributos.time);
145
+ if (!Number.isFinite(segundos) || segundos < 0)
146
+ return erro(`duracao invalida: ${atributos.time}`);
147
+ const milissegundos = segundos * 1000;
148
+ if (!Number.isFinite(milissegundos) || milissegundos > Number.MAX_SAFE_INTEGER) {
149
+ return erro('duracao fora do intervalo aceito');
150
+ }
151
+ return Math.round(milissegundos * 1000) / 1000;
152
+ }
153
+ function identidade(atributos) {
154
+ const nome = atributos.name?.trim();
155
+ if (!nome)
156
+ return erro('testcase sem name');
157
+ const prefixo = (atributos.classname || atributos.file || '').trim();
158
+ return prefixo ? `${prefixo} :: ${nome}` : nome;
159
+ }
160
+ function conferirIdentidadePersistivel(caso) {
161
+ if (caso.estado !== 'falhou' && caso.estado !== 'erro')
162
+ return;
163
+ if (caso.nome.length > 200 || (0, redacao_url_1.redigirCredenciaisUrl)(caso.nome) !== caso.nome || (0, policies_1.procurarSegredos)(caso.nome).length > 0) {
164
+ return erro('identidade de testcase nao pode ser persistida com seguranca');
165
+ }
166
+ }
167
+ /** Le pytest, Vitest, Jest, Playwright e gotestsum pelo contrato JUnit comum. */
168
+ function lerJUnitXml(xml) {
169
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(xml))
170
+ return erro('caractere de controle nao aceito');
171
+ xml = xml.replace(/\r\n?/g, '\n');
172
+ const pilha = [];
173
+ const casos = [];
174
+ let atual = null;
175
+ let raiz = null;
176
+ let viuSuite = false;
177
+ let suitesAbertas = 0;
178
+ let pos = xml.charCodeAt(0) === 0xfeff ? 1 : 0;
179
+ const marcar = (estado) => {
180
+ if (!atual)
181
+ return;
182
+ const peso = { passou: 0, pulado: 1, falhou: 2, erro: 3 };
183
+ if (peso[estado] > peso[atual.estado])
184
+ atual.estado = estado;
185
+ };
186
+ while (pos < xml.length) {
187
+ const abre = xml.indexOf('<', pos);
188
+ const texto = abre < 0 ? xml.slice(pos) : xml.slice(pos, abre);
189
+ if (pilha.length === 0 && texto.trim())
190
+ return erro('texto fora da raiz');
191
+ if (pilha.length > 0 && texto)
192
+ decodificar(texto);
193
+ if (abre < 0) {
194
+ pos = xml.length;
195
+ break;
196
+ }
197
+ if (xml.startsWith('<!--', abre)) {
198
+ const fim = xml.indexOf('-->', abre + 4);
199
+ if (fim < 0)
200
+ return erro('comentario sem fechamento');
201
+ pos = fim + 3;
202
+ continue;
203
+ }
204
+ if (xml.startsWith('<![CDATA[', abre)) {
205
+ const fim = xml.indexOf(']]>', abre + 9);
206
+ if (fim < 0)
207
+ return erro('CDATA sem fechamento');
208
+ pos = fim + 3;
209
+ continue;
210
+ }
211
+ if (xml.startsWith('<?', abre)) {
212
+ const fim = xml.indexOf('?>', abre + 2);
213
+ if (fim < 0)
214
+ return erro('instrucao sem fechamento');
215
+ pos = fim + 2;
216
+ continue;
217
+ }
218
+ if (xml.startsWith('<!', abre))
219
+ return erro('declaracao ou DTD nao aceita');
220
+ const fim = fimDaTag(xml, abre + 1);
221
+ const conteudo = xml.slice(abre + 1, fim);
222
+ if (conteudo.startsWith('/')) {
223
+ const nome = conteudo.slice(1).trim();
224
+ if (!NOME_XML.test(nome) || NOME_XML.exec(nome)?.[0] !== nome)
225
+ return erro('fechamento invalido');
226
+ const esperado = pilha.pop();
227
+ if (esperado !== nome)
228
+ return erro(`fechamento </${nome}> nao corresponde a <${esperado ?? 'ausente'}>`);
229
+ if (nomeLocal(nome) === 'testcase') {
230
+ if (!atual)
231
+ return erro('testcase fechado sem abertura');
232
+ casos.push(atual);
233
+ atual = null;
234
+ }
235
+ if (nomeLocal(nome) === 'testsuite')
236
+ suitesAbertas--;
237
+ pos = fim + 1;
238
+ continue;
239
+ }
240
+ const inicio = lerInicio(conteudo);
241
+ const local = nomeLocal(inicio.nome);
242
+ if (!raiz) {
243
+ if (local !== 'testsuite' && local !== 'testsuites')
244
+ return erro(`raiz <${inicio.nome}> nao e JUnit`);
245
+ raiz = inicio.nome;
246
+ }
247
+ else if (pilha.length === 0)
248
+ return erro('mais de uma raiz');
249
+ if (local === 'testsuite')
250
+ viuSuite = true;
251
+ if (local === 'testcase') {
252
+ if (atual)
253
+ return erro('testcase aninhado');
254
+ if (suitesAbertas === 0)
255
+ return erro('testcase fora de testsuite');
256
+ atual = { nome: identidade(inicio.atributos), estado: 'passou', duracaoMs: duracao(inicio.atributos) };
257
+ }
258
+ else if (local === 'failure')
259
+ marcar('falhou');
260
+ else if (local === 'error')
261
+ marcar('erro');
262
+ else if (local === 'skipped')
263
+ marcar('pulado');
264
+ if (inicio.autocontido) {
265
+ if (local === 'testcase') {
266
+ casos.push(atual);
267
+ atual = null;
268
+ }
269
+ }
270
+ else {
271
+ pilha.push(inicio.nome);
272
+ if (local === 'testsuite')
273
+ suitesAbertas++;
274
+ }
275
+ pos = fim + 1;
276
+ }
277
+ if (pilha.length > 0)
278
+ return erro(`elemento <${pilha.at(-1)}> sem fechamento`);
279
+ if (!raiz || !viuSuite)
280
+ return erro('nenhuma testsuite');
281
+ casos.forEach(conferirIdentidadePersistivel);
282
+ const contar = (estado) => casos.filter((c) => c.estado === estado).length;
283
+ const duracaoMs = casos.reduce((soma, caso) => soma + caso.duracaoMs, 0);
284
+ if (!Number.isSafeInteger(Math.round(duracaoMs)))
285
+ return erro('duracao somada fora do intervalo aceito');
286
+ return {
287
+ casos,
288
+ testes: casos.length,
289
+ falhas: contar('falhou'),
290
+ erros: contar('erro'),
291
+ pulados: contar('pulado'),
292
+ duracaoMs: Math.round(duracaoMs * 1000) / 1000,
293
+ };
294
+ }
295
+ function dentro(raiz, alvo) {
296
+ const relativo = path.relative(raiz, alvo);
297
+ return relativo !== '..' && !relativo.startsWith(`..${path.sep}`) && !path.isAbsolute(relativo);
298
+ }
299
+ function evidencia(caminho, estado, motivo) {
300
+ return { caminho, estado, motivo: motivo.slice(0, 300) };
301
+ }
302
+ /** O caminho declarado para o nome interno do comando (`test:<lane>` no Agile). */
303
+ function caminhoJUnitDoComando(manifesto, nome) {
304
+ if (nome.startsWith('test:'))
305
+ return manifesto.check?.agile?.junit?.[nome.slice('test:'.length)];
306
+ return manifesto.verify.junit?.[nome];
307
+ }
308
+ /** A marcacao fechada que acompanha o relatorio do mesmo comando ou lane. */
309
+ function marcacaoDeRelogioDoComando(manifesto, nome) {
310
+ if (nome.startsWith('test:'))
311
+ return manifesto.check?.agile?.relogio?.[nome.slice('test:'.length)];
312
+ return manifesto.verify.relogio?.[nome];
313
+ }
314
+ /** Nome e granular; marcador e tag declaram que o proprio comando ja selecionou aquela categoria. */
315
+ function casoMedeRelogio(nome, marcacao) {
316
+ if (!marcacao)
317
+ return false;
318
+ if ('marcador' in marcacao || 'tag' in marcacao)
319
+ return true;
320
+ return marcacao.nome.some((padrao) => {
321
+ try {
322
+ return new RegExp(padrao).test(nome);
323
+ }
324
+ catch {
325
+ return false;
326
+ }
327
+ });
328
+ }
329
+ function acessoAoDescritor(descritor) {
330
+ for (const base of ['/proc/self/fd', '/dev/fd']) {
331
+ const atalho = path.join(base, String(descritor));
332
+ try {
333
+ return { atalho, real: fs.realpathSync(atalho) };
334
+ }
335
+ catch { /* tenta o acesso por descritor que a plataforma oferece */ }
336
+ }
337
+ return null;
338
+ }
339
+ /**
340
+ * Abre o diretorio pai sem seguir o ultimo link e ancora as operacoes no descritor. O realpath do
341
+ * descritor detecta troca em qualquer ancestral; dali em diante, rename de caminho nao muda o pai.
342
+ */
343
+ function abrirDiretorioAncorado(absoluto, raizReal) {
344
+ const pai = path.dirname(absoluto);
345
+ let descritor;
346
+ try {
347
+ descritor = fs.openSync(pai, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
348
+ }
349
+ catch (e) {
350
+ const codigo = codigoDoErro(e);
351
+ return { ok: false, estado: codigo === 'ENOENT' ? 'ausente' : codigo === 'ELOOP' || codigo === 'ENOTDIR' ? 'inseguro' : 'erro',
352
+ motivo: `nao foi possivel ancorar o diretorio do relatorio (${codigo})` };
353
+ }
354
+ try {
355
+ const acesso = acessoAoDescritor(descritor);
356
+ if (!acesso) {
357
+ fs.closeSync(descritor);
358
+ return { ok: false, estado: 'erro', motivo: 'a plataforma nao oferece acesso ancorado ao relatorio' };
359
+ }
360
+ const info = fs.fstatSync(descritor);
361
+ const atual = fs.statSync(pai);
362
+ if (!info.isDirectory() || !dentro(raizReal, acesso.real) || atual.dev !== info.dev || atual.ino !== info.ino) {
363
+ fs.closeSync(descritor);
364
+ return { ok: false, estado: 'inseguro', motivo: 'o diretorio do relatorio mudou ou sai da worktree' };
365
+ }
366
+ return { ok: true, descritor, alvo: path.join(acesso.atalho, path.basename(absoluto)) };
367
+ }
368
+ catch (e) {
369
+ fs.closeSync(descritor);
370
+ return { ok: false, estado: 'erro', motivo: `nao foi possivel validar o diretorio do relatorio (${codigoDoErro(e)})` };
371
+ }
372
+ }
373
+ /** Le no maximo o teto mais um byte, suficiente para detectar crescimento sem alocacao aberta. */
374
+ function lerComLimite(descritor) {
375
+ const partes = [];
376
+ let total = 0;
377
+ while (total <= exports.LIMITE_JUNIT_BYTES) {
378
+ const tamanho = Math.min(64 * 1024, exports.LIMITE_JUNIT_BYTES + 1 - total);
379
+ if (tamanho === 0)
380
+ break;
381
+ const parte = Buffer.allocUnsafe(tamanho);
382
+ const lidos = fs.readSync(descritor, parte, 0, tamanho, null);
383
+ if (lidos === 0)
384
+ break;
385
+ partes.push(parte.subarray(0, lidos));
386
+ total += lidos;
387
+ }
388
+ return total > exports.LIMITE_JUNIT_BYTES ? { excedeu: true } : { bytes: Buffer.concat(partes, total), excedeu: false };
389
+ }
390
+ /** Remove a evidencia anterior antes do comando. Falha fechada: o comando ainda roda, mas o XML nao sera usado. */
391
+ function prepararRelatorioJUnit(cwd, caminho) {
392
+ const absoluto = path.resolve(cwd, caminho);
393
+ let raizReal = cwd;
394
+ try {
395
+ raizReal = fs.realpathSync(cwd);
396
+ }
397
+ catch {
398
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now(),
399
+ descarte: evidencia(caminho, 'inseguro', 'worktree sem realpath') };
400
+ }
401
+ const pai = abrirDiretorioAncorado(absoluto, raizReal);
402
+ if (!pai.ok) {
403
+ if (pai.estado === 'ausente')
404
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now() };
405
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now(),
406
+ descarte: evidencia(caminho, pai.estado, pai.motivo) };
407
+ }
408
+ try {
409
+ const anterior = fs.lstatSync(pai.alvo, { throwIfNoEntry: false });
410
+ if (!anterior)
411
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now() };
412
+ if (anterior.isDirectory()) {
413
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now(),
414
+ descarte: evidencia(caminho, 'inseguro', 'relatorio anterior nao e arquivo removivel') };
415
+ }
416
+ fs.unlinkSync(pai.alvo);
417
+ if (fs.lstatSync(pai.alvo, { throwIfNoEntry: false })) {
418
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now(),
419
+ descarte: evidencia(caminho, 'erro', 'relatorio anterior permaneceu depois da remocao') };
420
+ }
421
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now() };
422
+ }
423
+ catch (e) {
424
+ return { cwd, caminho, absoluto, raizReal, iniciadoEmMs: Date.now(),
425
+ descarte: evidencia(caminho, 'erro', `nao foi possivel remover o relatorio anterior (${codigoDoErro(e)})`) };
426
+ }
427
+ finally {
428
+ fs.closeSync(pai.descritor);
429
+ }
430
+ }
431
+ /** Le somente o arquivo novo e regular da rodada; qualquer duvida vira motivo de descarte. */
432
+ function lerRelatorioJUnit(preparado) {
433
+ if (preparado.descarte)
434
+ return { evidencia: preparado.descarte };
435
+ const { caminho, absoluto, raizReal } = preparado;
436
+ try {
437
+ const pai = abrirDiretorioAncorado(absoluto, raizReal);
438
+ if (!pai.ok) {
439
+ const estado = pai.estado === 'ausente' ? 'ausente' : pai.estado;
440
+ const motivo = pai.estado === 'ausente' ? 'o comando nao escreveu o relatorio' : pai.motivo;
441
+ return { evidencia: evidencia(caminho, estado, motivo) };
442
+ }
443
+ let bytes;
444
+ let descarteAposFechar;
445
+ let identidadeLida;
446
+ try {
447
+ const inicial = fs.lstatSync(pai.alvo, { throwIfNoEntry: false });
448
+ if (!inicial)
449
+ descarteAposFechar = evidencia(caminho, 'ausente', 'o comando nao escreveu o relatorio');
450
+ else if (!inicial.isFile() || inicial.isSymbolicLink()) {
451
+ descarteAposFechar = evidencia(caminho, 'inseguro', 'o relatorio nao e arquivo regular');
452
+ }
453
+ else {
454
+ let descritor;
455
+ try {
456
+ descritor = fs.openSync(pai.alvo, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
457
+ const info = fs.fstatSync(descritor);
458
+ const acesso = acessoAoDescritor(descritor);
459
+ const atual = fs.statSync(pai.alvo);
460
+ if (!acesso || !info.isFile() || !dentro(raizReal, acesso.real) ||
461
+ atual.dev !== info.dev || atual.ino !== info.ino) {
462
+ descarteAposFechar = evidencia(caminho, 'inseguro', 'o relatorio mudou durante a validacao de confinamento');
463
+ }
464
+ else {
465
+ identidadeLida = { dev: info.dev, ino: info.ino };
466
+ if (info.mtimeMs + TOLERANCIA_MTIME_MS < preparado.iniciadoEmMs) {
467
+ descarteAposFechar = evidencia(caminho, 'outra-rodada', 'o relatorio e anterior ao inicio do comando');
468
+ }
469
+ else if (info.size > exports.LIMITE_JUNIT_BYTES) {
470
+ descarteAposFechar = evidencia(caminho, 'invalido', `o relatorio excede ${exports.LIMITE_JUNIT_BYTES} bytes`);
471
+ }
472
+ else {
473
+ const lido = lerComLimite(descritor);
474
+ if (lido.excedeu)
475
+ descarteAposFechar = evidencia(caminho, 'invalido', `o relatorio excede ${exports.LIMITE_JUNIT_BYTES} bytes`);
476
+ else
477
+ bytes = lido.bytes;
478
+ const final = fs.fstatSync(descritor);
479
+ if (final.size !== info.size || final.mtimeMs !== info.mtimeMs || final.ctimeMs !== info.ctimeMs) {
480
+ descarteAposFechar = evidencia(caminho, 'inseguro', 'o relatorio mudou durante a leitura');
481
+ bytes = undefined;
482
+ }
483
+ }
484
+ }
485
+ }
486
+ catch (e) {
487
+ const estado = codigoDoErro(e) === 'ELOOP' ? 'inseguro' : 'erro';
488
+ descarteAposFechar = evidencia(caminho, estado, `nao foi possivel abrir o relatorio (${codigoDoErro(e)})`);
489
+ }
490
+ finally {
491
+ if (descritor !== undefined)
492
+ fs.closeSync(descritor);
493
+ }
494
+ }
495
+ const atual = fs.lstatSync(pai.alvo, { throwIfNoEntry: false });
496
+ if (identidadeLida && (!atual || !atual.isFile() || atual.isSymbolicLink() ||
497
+ atual.dev !== identidadeLida.dev || atual.ino !== identidadeLida.ino)) {
498
+ descarteAposFechar = evidencia(caminho, 'inseguro', 'o relatorio mudou antes da limpeza');
499
+ bytes = undefined;
500
+ }
501
+ if (atual && !atual.isDirectory())
502
+ fs.unlinkSync(pai.alvo);
503
+ }
504
+ finally {
505
+ fs.closeSync(pai.descritor);
506
+ }
507
+ if (descarteAposFechar)
508
+ return { evidencia: descarteAposFechar };
509
+ if (!bytes)
510
+ return { evidencia: evidencia(caminho, 'erro', 'o relatorio nao pode ser lido') };
511
+ let xml;
512
+ try {
513
+ xml = new node_util_1.TextDecoder('utf-8', { fatal: true }).decode(bytes);
514
+ }
515
+ catch {
516
+ return { evidencia: evidencia(caminho, 'invalido', 'o relatorio nao e UTF-8 valido') };
517
+ }
518
+ let relatorio;
519
+ try {
520
+ relatorio = lerJUnitXml(xml);
521
+ }
522
+ catch {
523
+ return { evidencia: evidencia(caminho, 'invalido', 'o relatorio nao contem XML JUnit valido') };
524
+ }
525
+ return { evidencia: { caminho, estado: 'lido', testes: relatorio.testes, falhas: relatorio.falhas,
526
+ erros: relatorio.erros, pulados: relatorio.pulados, duracaoMs: relatorio.duracaoMs }, relatorio };
527
+ }
528
+ catch (e) {
529
+ return { evidencia: evidencia(caminho, 'erro', `nao foi possivel ler o relatorio (${codigoDoErro(e)})`) };
530
+ }
531
+ }
532
+ /** Anexa estatisticas e, em comando vermelho, substitui o extrator de stdout pelos nomes completos do XML. */
533
+ function anexarRelatorioJUnit(resultado, preparado, marcacao) {
534
+ const lido = lerRelatorioJUnit(preparado);
535
+ if (!lido.relatorio)
536
+ return { ...resultado, junit: lido.evidencia };
537
+ if (resultado.ok)
538
+ return { ...resultado, junit: lido.evidencia };
539
+ const caidos = lido.relatorio.casos.filter((c) => c.estado === 'falhou' || c.estado === 'erro').map((c) => c.nome);
540
+ const base = { ...resultado, junit: lido.evidencia, testeQueCaiu: caidos,
541
+ falhasDeTeste: caidos.map((nome) => ({ nome, relogio: casoMedeRelogio(nome, marcacao) })) };
542
+ if (caidos.length > 0)
543
+ return base;
544
+ const { testeQueCaiu: _testes, falhasDeTeste: _falhas, ...semNomes } = base;
545
+ return semNomes;
546
+ }
@@ -54,6 +54,9 @@ exports.VERIFY_PT_BR = {
54
54
  agile: (reaproveitadas, inteiras, paralelo, lanes) => ` agile ${reaproveitadas} comando(s) de claim contra o produto do preparo, ${inteiras} inteiro(s); ` +
55
55
  `${paralelo ? `grupo em paralelo${lanes.length ? ` com as lanes ${lanes.join(', ')}` : ''}` : 'comandos do manifesto em serie'}`,
56
56
  agileFallback: (peca, motivo, fallback) => ` fallback ${peca}: ${motivo || 'sem motivo dito'}; rodou ${fallback || 'o equivalente do Classic'}`,
57
+ agileDegradado: (peca, motivo) => ` degradado ${peca}: ${motivo || 'sem motivo dito'}; preservou o plano paralelo`,
58
+ agilePeca: (peca, motivo) => ` agile ${peca}: ${motivo || 'sem motivo dito'}`,
59
+ reservaIntegracao: (cpus, fonte) => `reserva da integracao ${cpus} CPU(s), origem ${{ medida: 'medida', conservadora: 'conservadora', classic: 'Classic' }[fonte]}`,
57
60
  preparo: (situacao, segundos, alterado) => ` preparo ${situacao} em ` +
58
61
  `${segundos} s, uma vez para a rodada; produto ` +
59
62
  `${alterado ? 'MUDOU no meio da rodada: nenhum veredito vale' : 'conferido do preparo ao fim'}`,
@@ -73,6 +76,21 @@ exports.VERIFY_PT_BR = {
73
76
  comandosDoManifesto: 'Comandos de verify do manifesto:',
74
77
  comandoOk: 'ok',
75
78
  comando: (nome, situacao, comando) => ` ${nome.padEnd(10)} ${situacao} ${comando}`,
79
+ rodadaParcial: (testes) => ` rodada PARCIAL; teste(s) filtrado(s): ${testes}`,
80
+ selecaoDaLane: (lane, modo, motivo, arquivos) => {
81
+ const razoes = {
82
+ 'rodada-fria': 'rodada fria', 'cache-ausente': 'cache anterior ausente', 'cache-invalido': 'cache anterior invalido',
83
+ 'base-mudou': 'base mudou', 'manifesto-mudou': 'manifesto mudou', 'lockfile-mudou': 'lockfile mudou',
84
+ 'configuracao-mudou': 'configuracao do runner mudou', 'fora-do-mapeamento': 'mudanca fora do mapeamento do runner',
85
+ 'runner-sem-garantia': 'runner sem garantia declarada', 'sem-mudancas': 'nenhuma mudanca verificavel',
86
+ selecionada: 'cache valido e mudancas mapeadas',
87
+ };
88
+ return ` lane ${lane} ${modo === 'selecionada' ? 'SELECIONADA' : 'INTEIRA'}: ` +
89
+ `${razoes[motivo] ?? motivo}${arquivos ? ` (${arquivos})` : ''}`;
90
+ },
91
+ retesteDeRelogio: (testes, steal, estado, code) => ` relogio protegido: ${testes}; steal ${steal}%; segunda rodada ${estado === 'sem-filtro'
92
+ ? 'nao executada (filtro ausente)' : estado === 'sem-veredito' ? `sem veredito (codigo ${code ?? 'ausente'})`
93
+ : `${estado} (codigo ${code ?? 'ausente'})`}`,
76
94
  testesQueCairam: (testes) => ` teste(s) que caiu(ram): ${testes}`,
77
95
  estourou: (prazo, rodou) => `ESTOUROU O PRAZO (${prazo}, rodou ${rodou})`,
78
96
  relogioSobSteal: (pct, limiar) => `RELOGIO SOB STEAL (${pct}% de steal, acima de ${limiar}%)`,
@@ -155,6 +173,10 @@ exports.VERIFY_EN = {
155
173
  agile: (reaproveitadas, inteiras, paralelo, lanes) => ` agile ${reaproveitadas} claim command(s) against the prepared product, ${inteiras} in full; ` +
156
174
  `${paralelo ? `parallel group${lanes.length ? ` with lanes ${lanes.join(', ')}` : ''}` : 'manifest commands in series'}`,
157
175
  agileFallback: (peca, motivo, fallback) => ` fallback ${peca}: ${motivo || 'no reason given'}; ran ${fallback || 'the Classic equivalent'}`,
176
+ agileDegradado: (peca, motivo) => ` degraded ${peca}: ${motivo || 'no reason given'}; kept the parallel plan`,
177
+ agilePeca: (peca, motivo) => ` agile ${peca}: ${motivo || 'no reason given'}`,
178
+ reservaIntegracao: (cpus, fonte) => `integration reservation ${cpus} CPU(s), ` +
179
+ `source ${{ medida: 'measured', conservadora: 'conservative', classic: 'Classic' }[fonte]}`,
158
180
  preparo: (situacao, segundos, alterado) => ` prepare ${situacao} in ${segundos} s, once for the run; product ` +
159
181
  `${alterado ? 'CHANGED during the run: no verdict counts' : 'checked from prepare to end'}`,
160
182
  stealMedido: (pct, fonte, limiar) => ` steal ${pct}% in the run (${fonte}; above ${limiar}%, a failing clock-bound test gets no verdict)`,
@@ -171,6 +193,21 @@ exports.VERIFY_EN = {
171
193
  comandosDoManifesto: 'Manifest verify commands:',
172
194
  comandoOk: 'ok',
173
195
  comando: (nome, situacao, comando) => ` ${nome.padEnd(10)} ${situacao} ${comando}`,
196
+ rodadaParcial: (testes) => ` PARTIAL run; filtered test(s): ${testes}`,
197
+ selecaoDaLane: (lane, modo, motivo, arquivos) => {
198
+ const razoes = {
199
+ 'rodada-fria': 'cold run', 'cache-ausente': 'previous cache missing', 'cache-invalido': 'previous cache invalid',
200
+ 'base-mudou': 'base changed', 'manifesto-mudou': 'manifest changed', 'lockfile-mudou': 'lockfile changed',
201
+ 'configuracao-mudou': 'runner configuration changed', 'fora-do-mapeamento': 'change outside runner mapping',
202
+ 'runner-sem-garantia': 'runner has no declared guarantee', 'sem-mudancas': 'no verifiable changes',
203
+ selecionada: 'valid cache and mapped changes',
204
+ };
205
+ return ` lane ${lane} ${modo === 'selecionada' ? 'SELECTED' : 'FULL'}: ` +
206
+ `${razoes[motivo] ?? motivo}${arquivos ? ` (${arquivos})` : ''}`;
207
+ },
208
+ retesteDeRelogio: (testes, steal, estado, code) => ` protected clock test(s): ${testes}; ${steal}% steal; second run ${estado === 'sem-filtro'
209
+ ? 'not run (no filter)' : estado === 'sem-veredito' ? `had no verdict (exit ${code ?? 'missing'})`
210
+ : `${estado === 'passou' ? 'passed' : 'failed'} (exit ${code ?? 'missing'})`}`,
174
211
  testesQueCairam: (testes) => ` failing test(s): ${testes}`,
175
212
  estourou: (prazo, rodou) => `TIMED OUT (${prazo}, ran ${rodou})`,
176
213
  relogioSobSteal: (pct, limiar) => `CLOCK UNDER STEAL (${pct}% steal, above ${limiar}%)`,